从零开始写 Docker(八)---实现 mydocker run -d 支持后台运行容器

本文为从零开始写 Docker 系列第八篇,实现类似 docker run -d 的功能,使得容器能够后台运行。
完整代码见:https://github.com/lixd/mydocker
欢迎 Star
推荐阅读以下文章对 docker 基本实现有一个大致认识:
- 核心原理:深入理解 Docker 核心原理:Namespace、Cgroups 和 Rootfs
- 基于 namespace 的视图隔离:探索 Linux Namespace:Docker 隔离的神奇背后
- 基于 cgroups 的资源限制
- 基于 overlayfs 的文件系统:Docker 魔法解密:探索 UnionFS 与 OverlayFS
- 基于 veth pair、bridge、iptables 等等技术的 Docker 网络:揭秘 Docker 网络:手动实现 Docker 桥接网络
开发环境如下:
root@mydocker:~# lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 20.04.2 LTS
Release: 20.04
Codename: focal
root@mydocker:~# uname -r
5.4.0-74-generic
注意:需要使用 root 用户
1. 概述
经过前面的 7 篇文章,我们已经基本实现了一个简单的 docker 了。
不过与 Docker 创建的容器相比,我们还缺少以下功能
- 1)指定后台运行容器,也就是 detach 功能
- 2)通过 docker ps 查看目前处于运行中的容器
- 3)通过docker logs 查看容器的输出
- 4)通过 docker exec 进入到一个已经创建好了的容器中
后续几篇文章主要就是一一实现这些功能,本文首先实现 mydocker run -d 让容器后台运行。
2. 原理分析
在 Docker 早期版本,所有的容器 init 进程都是从 docker daemon 这个进程 fork 出来的,这也就会导致一个众所周知的问题,如果 docker daemon 挂掉,那么所有的容器都会宕掉,这给升级 docker daemon 带来很大的风险。
子进程的结束和父进程的运行是一个异步的过程,即父进程永远不知道子进程到底什么时候结束。如果创建子进程的父进程退出,那么这个子进程就成了没人管的孩子,俗称孤儿进程。为了避免孤儿进程退出时无法释放所占用的资源而僵死,进程号为 1 的 init 进程就会接受这些孤儿进程。
即:Docker 早期架构中,docker daemon挂掉后,所有容器作为子进程都会被 init 进程托管,实际上还是可以运行的,但是 docker daemon 挂了会导致他维护的一些资源也没了,所以容器实际上是不能正常运行的。
为了解决该问题后来,Docker 使用了 containerd, 负责管理容器的生命周期,包括创建、运行、停止等。同时 containerd 为每个进程都启动了一个 init 进程(图中的 containerd-shim),containerd-shim 进程负责接收来自 containerd 的命令,启动容器中的进程,并监控它们的生命周期。
便可以实现即使 daemon 挂掉,容器依然健在的功能了,其结构如下图所示。

为了简单起见,我们就按照 Docker 早期架构实现吧。在我们的实现中:
- 当前运行命令的 mydocker 是主进程
- 容器是被当前 mydocker 进程 fork 出来的子进程。
这样看来,mydocker 可以看做是图中的 containerd,mydocker 中具体实现 Namespace 隔离,cgroups 资源限制的部分代码则可以看做是 runC或者 libcontainer。
具体实现就是,fork 出子进程后,mydocker 进程直接退出掉。是当 mydocker 进程退出后,容器进程就会被 init 进程接管,这时容器进程还是运行着的。
也算是实现了一个简易版本的后台运行。
3. 实现
首先,需要在 main-command.go 里面添加 -d flag,表示这个容器启动的时候后台在运行:
var runCommand = cli.Command{
Name: "run",
Usage: `Create a container with namespace and cgroups limit
mydocker run -it [command]`,
Flags: []cli.Flag{
cli.BoolFlag{
Name: "it", // 简单起见,这里把 -i 和 -t 参数合并成一个
Usage: "enable tty",
},
cli.BoolFlag{
Name: "d",
Usage: "detach container",
},
// 省略其他代码
},
/*
这里是run命令执行的真正函数。
1.判断参数是否包含command
2.获取用户指定的command
3.调用Run function去准备启动容器:
*/
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和detach只能同时生效一个
tty := context.Bool("it")
detach := context.Bool("d")
if tty && detach {
return fmt.Errorf("it and d paramter can not both provided")
}
resConf := &subsystems.ResourceConfig{
MemoryLimit: context.String("mem"),
CpuSet: context.String("cpuset"),
CpuCfsQuota: context.Int("cpu"),
}
volume := context.String("v")
Run(tty, cmdArray, resConf, volume)
return nil
},
}
然后调整 Run 方法,只有指定 tty 的时候才执行 parent.Wait。
parent.Wait() 主要是用于父进程等待子进程结束,这在交互式创建容器的步骤里面是没问题的,但是指定了 -d要后台运行就不能再去等待,创建容器之后,父进程直接退出即可。
func Run(tty bool, comArray []string, res *subsystems.ResourceConfig, volume string) {
parent, writePipe := container.NewParentProcess(tty, volume)
if parent == nil {
log.Errorf("New parent process error")
return
}
if err := parent.Start(); err != nil {
log.Errorf("Run parent.Start err:%v", err)
return
}
// 创建cgroup manager, 并通过调用set和apply设置资源限制并使限制在容器上生效
cgroupManager := cgroups.NewCgroupManager("mydocker-cgroup")
defer cgroupManager.Destroy()
_ = cgroupManager.Set(res)
_ = cgroupManager.Apply(parent.Process.Pid, res)
// 在子进程创建后才能通过pipe来发送参数
sendInitCommand(comArray, writePipe)
if tty { // 如果是tty,那么父进程等待,就是前台运行,否则就是跳过,实现后台运行
_ = parent.Wait()
container.DeleteWorkSpace("/root/", volume)
}
}
4. 测试
运行一个 top 命令:
root@mydocker:~/feat-run-d/mydocker# go build .
root@mydocker:~/feat-run-d/mydocker# ./mydocker run -d top
{"level":"info","msg":"createTty false","time":"2024-01-24T16:58:16+08:00"}
{"level":"info","msg":"resConf:\u0026{ 0 }","time":"2024-01-24T16:58:16+08:00"}
{"level":"info","msg":"busybox:/root/busybox busybox.tar:/root/busybox.tar","time":"2024-01-24T16:58:16+08:00"}
{"level":"info","msg":"mount overlayfs: [/usr/bin/mount -t overlay overlay -o lowerdir=/root/busybox,upperdir=/root/upper,workdir=/root/work /root/merged]","time":"2024-01-24T16:58:16+08:00"}
{"level":"info","msg":"command all is top","time":"2024-01-24T16:58:16+08:00"}
可以看到,mydocker 命令直接退出了。
使用 top 作为容器内前台进程。然后在宿主机上执行 ps -ef 看一下 建的容器进程是否存在:
root@mydocker:~/feat-run-d/mydocker# ps -ef|grep -e PPID -e top
UID PID PPID C STIME TTY TIME CMD
root 166637 1 0 16:5 pts/8 00:00:00 top
可以看到,top 命令的进程正在运行着,它的父进程是 1。
这说因为mydocker 主进程退出了,但是 fork 出来的容器子进程依然存在,由于父进程消失,它就被 PID为 1 的 init 进程给托管了,由此就实现了 mydocker run -d 命令,即容器的后台运行。
4. 总结
本篇实现的 mydocker run -d 比较简单,就是启动完子进程(容器)后,直接退出父进程,让 init 进程去接管子进程。
不过现在比较大的问题是,虽然容器在后台运行了,但是已经找不到了,因此下一篇需要实现 mydocker ps 命令来查看运行中的容器。
【从零开始写 Docker 系列】持续更新中,搜索公众号【探索云原生】订阅,阅读更多文章。

完整代码见:https://github.com/lixd/mydocker
欢迎 Star
相关代码见 feat-volume 分支,测试脚本如下:
需要提前在 /root 目录准备好 busybox.tar 文件,具体见第四篇第二节。
# 克隆代码
git clone -b feat-run-d https://github.com/lixd/mydocker.git
cd mydocker
# 拉取依赖并编译
go mod tidy
go build .
# 测试
./mydocker run top -d
从零开始写 Docker(八)---实现 mydocker run -d 支持后台运行容器的更多相关文章
- centos7下安装docker(8.1运行容器)
从今天开始学习docker container 1.docker run 之前我们在学习制作镜像以及制作私有仓库的时候已经用到docker run -it以及docker run -d来临时运行一个容 ...
- 从零开始写STL-容器-list
从零开始写STL-容器-list List 是STL 中的链表容器,今天我们将通过阅读和实现list源码来解决一下问题: List内部的内存结构是如何实现的? 为什么List的插入复杂度为O(1)? ...
- 从零开始学习docker之在docker中搭建redis(单机)
docker搭建redis 一.环境准备 云环境:CentOS 7.6 64位 二.下载镜像 从docker hub中找到redis镜像 传送门------https://hub.docker.com ...
- 3、docker常用命令:help、镜像命令、容器命令
1.帮助命令 1.docker version 2.docker info 3.重点掌握:docker --help 2.镜像命令 1.docker,镜像,容器关系 2.docker images ( ...
- 从零开始构建docker基础镜像
段子 今年基本已经结束了,我问了很多朋友今年挣钱了没?大多朋友都有挣,而且挣得五花八门:有挣个屁的,有挣个锤子的,有挣个毛的,更有甚者挣个妹的,奢侈之极!最恐怖的是挣个鬼的!有的还可以,挣个球,下午我 ...
- 从零开始学习 Docker
这篇文章是我学习 Docker 的记录,大部分内容摘抄自 <<Docker - 从入门到实践>> 一书,并非本人原创.学习过程中整理成适合我自己的笔记,其中也包含了我自己的 ...
- 从零开始搭建Docker Swarm集群
从零开始搭建Docker Swarm集群 检查节点Docker配置 1. 打开Docker配置文件(示例是centos 7)vim /etc/sysconfig/docker2. 添加-H tcp:/ ...
- Docker学习总结之Run命令介绍
Docker学习总结之Run命令介绍 本文由Vikings(http://www.cnblogs.com/vikings-blog/) 原创,转载请标明.谢谢! 在使用Docker时,执行最多的命令某 ...
- 一起学习造轮子(一):从零开始写一个符合Promises/A+规范的promise
本文是一起学习造轮子系列的第一篇,本篇我们将从零开始写一个符合Promises/A+规范的promise,本系列文章将会选取一些前端比较经典的轮子进行源码分析,并且从零开始逐步实现,本系列将会学习Pr ...
- 从零开始写一个npm包及上传
最近刚好自己需要写公有npm包及上传,虽然百度上资料都能找到,但是都是比较零零碎碎的,个人就来整理下,如何从零开始写一个npm包及上传. 该篇文件只记录一个大概的流程,一些细节没有记录. tips: ...
随机推荐
- JS leetcode 存在重复元素 II 题解分析,记一次震惊的负向优化
壹 ❀ 引 整理下今天做的算法题,题目难度不高,但在优化角度也是费了一些功夫.题目来自219. 存在重复元素 II,问题描述如下: 给定一个整数数组和一个整数 k,判断数组中是否存在两个不同的索引 i ...
- NC13950 Alliances
题目链接 题目 题目描述 树国是一个有n个城市的国家,城市编号为1∼n.连接这些城市的道路网络形如一棵树, 即任意两个城市之间有恰好一条路径.城市中有k个帮派,编号为1∼k.每个帮派会占据一些城市,以 ...
- Python Split 函数用法
一.split函数简介Python中split()函数,具体作用如下: 拆分字符串.通过指定分隔符对字符串进行切片,并返回分割后的字符串列表(list):二.语法split() 方法语法: str.s ...
- Python中保存字典类型数据到文件
三种方法: 1.在 Python 中使用 pickle 模块的 dump 函数将字典保存到文件中import pickle my_dict = { 'Apple': 4, 'Banana': 2, ' ...
- C语言,函数形参与实参个数不一致问题
最近阅读工程代码的时候,同一个函数,不同场景调用时,输入的实参个数不一样,但是编译却没有问题.查看函数的定义,相关的C文件里并没有给形参指定默认值,这就很奇怪了. 最终,发现在函数相关的头文件 ...
- CSS实现展开动画
CSS实现展开动画 展开收起效果是比较常见的一种交互方式,通常的做法是控制display属性值在none和其它值之间切换,虽说功能可以实现,但是效果略显生硬,所以会有这样的需求--希望元素展开收起能具 ...
- SpringBoot+Shiro+LayUI权限管理系统项目-2.业务模型分析
1.项目模型介绍 1.1 部门表 部门编码.部门名称.上级部门 1.2 角色表 角色编码.角色名称 1.3 权限表 权限名称.权限标识.权限类型.上级权限.URL.权限图标.是否外部打开 1.4 用户 ...
- python课本学习第六章
一.字典的概念 #示例代码 student = {'name':'xx','name':'yy','grade1':98.1,'grade':99.2} print(student) #output: ...
- Git 分支管理参考模型
一个值得参考的Git分支管理模型如下: master 生产主分支,发布到生产环境使用这个分支,由hotfix或者release分支合并过来,不直接提交代码. release 预发布分支, 基于feat ...
- CSDN的Markdown编辑器使用说明
这里写自定义目录标题 欢迎使用Markdown编辑器 新的改变 功能快捷键 合理的创建标题,有助于目录的生成 如何改变文本的样式 插入链接与图片 如何插入一段漂亮的代码片 生成一个适合你的列表 创建一 ...