一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情。
简介
在前几篇中,我们已经构建了一个基础的镜像,本篇开始做一些进阶的功能,下面就是实现docker中的-d命令,让容器能够后台运行
源码说明
同时放到了Gitee和Github上,都可进行获取
本章节对应的版本标签是:5.1,防止后面代码过多,不好查看,可切换到标签版本进行查看
代码实现
在Docker早期版本,所有的容器init进程都是从dockerdaemon这个进程fork出来的。
这也就会导致一个众所周知的问题,如果dockerdaemon挂掉,那么所有的容器都会宕掉,这给升级docker daemon带来很大的风险。
后来,Docker使用了containerd,也就是现在的runC,便可以实现即使daemon挂掉,容器依然健在的功能了
我们并不想去实现一个daemon,因为这和容器的关联不是特别大,而且,查看Docker的运行引擎runC可以发现,runC也提供一种detach功能,可以保证在runC退出的情况下容器依然可以运行。
因此,我们将会使用detach功能去实现创建完成容器后,mydocker就会退出,但是容器依然继续运行的功能。
容器,在操作系统看来,其实就是一个进程。当前运行命令的mydocker 是主进程,容器是被当前mydocker进程fork出来的子进程。
子进程的结束和父进程的运行是一个异步的过程,即父进程永远不知道子进程到底什么时候结束。
如果创建子进程的父进程退出,那么这个子进程就成了没人管的孩子,俗称孤儿进程。
为了避免孤儿进程退出时无法释放所占用的资源而僵死,进程号为1的进程init就会接受这些孤儿进程。
这就是父进程退出而容器进程依然运行的原理。
虽然容器刚开始是由当前运行的mydocker进程创建的,但是当mydocker进程退出后,容器进程就会被进程号为1的init进程接管,这时容器进程还是运行着的,这样就实现了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