自己动手写Docker系列 -- 5.1实现容器的后台运行
时间:2023-04-21 03:37:00
简介
在前几篇文章中,我们建立了一个基本的镜像。本文开始做一些高级功能。以下是实现docker中的-d命令允许容器在后台运行
源码说明
同时放到了Gitee和Github上,都可以获得
- Gitee: https://gitee.com/free-love/docker-demo
- GitHub: https://github.com/lw1243925457/dockerDemo
本章对应的版本标签为:5.1.防止后面的代码太多,难以查看,可以切换到标签版本查看
代码实现
在Docker早期版本,所有容器init进程都是从dockerdaemon这个进程fork出来的。
如果这也会导致一个众所周知的问题dockerdaemon如果挂断,所有容器都会停止升级docker daemon风险很大。
后来,Docker使用了containerd,也就是现在runC,即使可以实现daemon挂断,容器还活着的功能
我们不想实现一个daemon,因为这和容器关系不是特别大,查看Docker的运行引擎runC可以发现,runC也提供一种detach功能,可以保证runC容器在退出时仍能运行。
因此,我们将会使用detach创建完成容器后,功能实现,mydocker它将退出,但容器仍在运行。
在操作系统看来,容器实际上是一个过程。当前的操作命令mydocker 是主过程,容器是当前的mydocker进程fork子过程出来。
子过程的结束和父过程的运行是一个异步过程,即父过程永远不知道子过程何时结束。
如果创建子过程的父过程退出,那么这个子过程就成了无人管理的孩子,俗称孤儿过程。
为避免孤儿退出时无法释放占用的资源而死亡,过程号为1init接受这些孤儿进程。
这就是父进程退出和容器进程仍在运行的原理。
虽然容器最初是由当前运行的mydocker进程创建的,但是当mydocker退出过程后,进程号为1init接管过程时,容器过程仍在运行,从而实现mydocker退出功能,容器不功能。
原理大致如上,相应的实现代码如下:
1.首先增加-d并传入命令选项run中
var RunCommand = cli.Command{
Name: "run", Usage: `Create a container with namespace and cgroups limit mydocker run -ti [command]`, Flags: []cli.Flag{
...... // 添加-d标签 cli.BoolFlag{
Name: "d", Usage: "detach container", }, }, Action: func(context *cli.Context) error {
if len(context.Args()) < 1 {
return fmt.Errorf("missing container command") } var cmdArray []string for _, arg := range context.Args() {
cmdArray = append(cmdArray, arg) } tty := context.Bool("ti") detach := context.Bool("d") resConfig := &subsystem.ResourceConfig{
MemoryLimit: context.String("mem"), CpuShare: context.String("cpuShare"), CpuSet: context.String("cpuSet"), } volume := context.String("v") run.Run(tty, detach, cmdArray, resConfig, volume) return nil }, }
2.在run中,如果detach不为true则一直等待父进程退出,不反之,则父进程退出,docker进行成为孤儿进程,让init进程进行接管
func Run(tty, detach bool, cmdArray []string, config *subsystem.ResourceConfig, volume string) {
.....
log.Infof("parent process run")
// 和书中稍微有点不一样,我们这里直接判断detach即可
// 在我们使用docker的时候,-dit是可以共存的,所以我们优先判断detach
if !detach {
_ = parent.Wait()
deleteWorkSpace(rootUrl, mntUrl, volume)
}
os.Exit(-1)
}
3.对文件空间初始化的改造
在完成上面的代码后,是可以运行了,但如果我们强杀进程后,其挂载点和可写层并没有进行清理
查看了书中提供的整个源码,应该是后面的rm命令实现的
而我们在实际使用docker的过程中,好像不删除容器,直接使用容器ID重启后,上次的文件是有进行保存的,还存在
所有我们对原来的文件空间初始化进行改造,让我们强杀容器进程后,能够再次启动进程,而不是报文件夹已存在的错误
如下所示,当挂载点和读写层不存在时,我们才进行创建
func createWriteLayer(rootUrl string) error {
writeUrl := rootUrl + "writeLayer/"
exist, err := pathExist(writeUrl)
if err != nil && !os.IsNotExist(err) {
return err
}
if !exist {
if err := os.Mkdir(writeUrl, 0777); err != nil {
return fmt.Errorf("create write layer failed: %v", err)
}
}
return nil
}
func createMountPoint(rootUrl string, mntUrl string) error {
// 创建mnt文件夹作为挂载点
exist, err := pathExist(mntUrl)
if err != nil && !os.IsNotExist(err) {
return err
}
if !exist {
if err := os.Mkdir(mntUrl, 0777); err != nil {
return fmt.Errorf("mkdir faild: %v", err)
}
}
// 把writeLayer和busybox目录mount到mnt目录下
dirs := "dirs=" + rootUrl + "writeLayer:" + rootUrl + "busybox"
cmd := exec.Command("mount", "-t", "aufs", "-o", dirs, "none", mntUrl)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("mmt dir err: %v", err)
}
return nil
}
运行测试
我们如书中的例子,运行一个top命令来进行测试
➜ dockerDemo git:(main) ✗ go build mydocker/main.go
➜ dockerDemo git:(main) ✗ ./main run -d top
{
"level":"info","msg":"memory cgroup path: /sys/fs/cgroup/memory/mydocker-cgroup","time":"2022-03-21T05:58:44+08:00"}
{
"level":"info","msg":"memory cgroup path: /sys/fs/cgroup/memory/mydocker-cgroup","time":"2022-03-21T05:58:44+08:00"}
{
"level":"info","msg":"all command is : top","time":"2022-03-21T05:58:44+08:00"}
{
"level":"info","msg":"parent process run","time":"2022-03-21T05:58:44+08:00"}
➜ dockerDemo git:(main) ✗ ps -ef |grep top
root 69016 1 0 05:58 pts/0 00:00:00 top
root 69044 5508 0 05:58 pts/0 00:00:00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn --exclude-dir=.idea --exclude-dir=.tox top
➜ dockerDemo git:(main) ✗
可以看到启动起来后就退出了,没有进入交互命令,查看top进程时,其父进程id是1