docker引擎的架构图:
DockerEngine
|
containerd
___________________|____________________
| | |
containerd-shim containerd-shim ...
| | |
runC runC ...
|
|
注意,容器的"单进程模型",并不是指容器里只能运行"一个"进程,而是指容器没有管理多个进程的能力。 这是因为容器里PID=1的进程就是应用本身,其他的进程都是这个PID=1进程的子进程。 可是,用户编写的应用(容器内PID=1的进程),并不能够像正常操作系统里的init进程或者systemd那样拥有进程管理的功能。
1. 实现容器的后台运行
容器,在操作系统看来,其实就是一个进程。当前运行命令的mydocker是主进程,容器是被当做mydocker进程fork出来的子进程。 子进程的结束和父进程的运行是一个异步的过程,即父进程永远不知道子进程到底什么时候结束。如果创建子进程的父进程退出, 那么这个子进程就成了没人管的孩子,俗称孤儿进程。为了避免孤儿进程退出时无法释放所占用的资源而僵死,进程号为1的进程 init就会接受这些孤儿进程。这就是父进程退出而容器进程依然运行的原理。
首先,需要给run子命令添加一个-d标签(bool类型),表示这个容器启动的时候以daemon的模式运行。注意,-d和-it不能共存的:
|
|
cmd.Wait()主要是用于父进程等待子进程结束,这在交互式创建容器的步骤里面是没有问题的,但是在这里,如果detach创建了容器, 就不能再去等待,创建容器之后,父进程就已经退出了。因此,这里只是将容器内的init进程启动起来,就已经完成工作,紧接着就 可以退出,然后由操作系统进程ID为1的init进程去接管容器进程。
实现分离的模式运行容器之后,会出现一个问题: 使用一个镜像启动的多个容器会使用同一个overlayFS文件系统,容器的可写层会互相影响。所以,这里要实现如下两个目的:
- 为每个容器分配单独的隔离文件系统
- 修改mydocker commit命令,实现对不同容器进行打包镜像的功能
修改test/0test.go
- 修改MntURL和WriteLayerUrl和WorkLayerUrl
- 给DeleteWorkSpace函数添加
imageName string
和containerName string
这两个参数
|
|
至此,便完成了为每个容器分配单独的隔离文件系统的工作,下面介绍对commit子命令的改造:
if len(os.Args) < 4 {
println("Missing container name and image name")
os.Exit(-1)
}
commitContainer(os.Args[2], os.Args[3])
接下来修改commitContainer函数,实际根据传入的containerName制作imageName.tar镜像:
|
|
- 给NewWorkSpace函数添加
imageName string
和containerName string
这两个参数
|
|
2. 实现mydocker ps的功能
首先,添加一个名叫"ps"的子命令,并且,当子命令为"ps"时,就直接执行下面的代码:
|
|
3. 实现查看容器日志
一般来说,对于容器中运行的进程,使日志达到标准输出是一个非常好的实现方案,因此需要将容器中的标准输出保存下来,以便需要的时候访问。这就是mydocker log命令的实现原理。
我们会将容器进程的标准输出挂载到"/var/run/mydocker/容器名/container.log"文件中,这样就可以在调用mydocker logs的时候去读取这个文件,并将进程内的标准输出打印出来。
首先需要修改0test.go文件,将容器进程的标准输出重定向一下。
|
|
然后添加一个名为"logs"的子命令,并添加一个参数(会有参数校验),并直接执行查询日志的函数:
logContainer的实现非常简单:
|
|
4. 实现进入容器Namespace(mydocker exec命令)
setns是一个系统调用,可以根据提供的PID再次进入到指定的Namespace中。它需要先打开/proc/[pid]/ns/文件夹下对应的文件,
然后使当前进程进入到指定的Namespace中。但是对于Mount Namespace来说,一个具有多线程的进程是无法使用setns调用进入到
对应的命令空间的。而Go每启动一个程序就会进入多线程状态,因此需要在启动Go运行时之前先执行setns系统调用,这就需要用到cgo了。
代码见test/nsenter.go。nsenter.go中使用了c中的__attribute__
构造函数,所以,一旦这个包被引入,它就会在所有Go运行的
环境启动之前执行,这样就避免了Go多线程导致的无法进入mnt Namespace的问题。
- Cgo Cgo是一个很炫酷的功能,允许Go程序去调用C的函数与标准库。你只需要以一种特殊的方式在Go的源代码里写出需要调用的C的代码, Cgo就可以把你的C源码文件和Go文件整合在一起。下面举一个最简单的例子,在这个例子中有两个函数————Random和Seed,在它们里面 调用了C的random和srandom函数。
这段代码导入了C,但是你会发现在Go标准库里面并没有这个包,那是因为这根本就不是一个真正的包,而只是Cgo创建的一个特殊命名空间, 用来与C的命名空间交流。这两个函数都分别调用了C里面的random和uint函数,然后对它们进行了类型转换。这就实现了Go代码里面调用C的功能。
- 实现exec命令 首先需要添加一个名叫"exec"的子命令,这个子命令至少要包含两个参数:
|
|
然后看下ExecContainer的实现:
|
|
这里又遇到了熟悉的/proc/self/exe, 只不过是换了后面的参数,由原来的init变成了现在的exec。 这么做的目的就是为了那段C代码的执行。因为一旦程序启动,那段C代码就会运行,那么对于我们使用exec来说, 当容器名和对应的命令传递过来以后,程序已经执行了,而且那段C代码也应该运行完毕。那么,怎么指定环境变量让它 再执行一遍呢?这里就用到了这个/proc/self/exe。 这里有创建了一个command,只不过这次只是简单地fork出来一个进程, 不需要这个进程拥有什么命名空间的隔离,然后把这个进程的标准输入输出都绑定到宿主机上。这样去run这里的进程时, 实际上就是又运行了一遍自己的程序,但是这时有一点不同的就是,再一次运行的时候已经指定了环境变量,所以C代码执行 的时候,就能拿到对应的环境变量,便可以进入到指定的Namespace中进行操作了。这时应该就可以明白前面一段代码的意义了:
这里就是第二次进入本程序执行exec的时候,如果已经指定了环境变量,说明C代码已经运行,直接返回就可以了,以免重复调用。
- 实现给run子命令指定环境变量 修改run子命令,添加一个-e的标签:
在cmd.ExtraFiles = []*os.File{readPipe}
下面添加一行:
|
|
这个时候,对于不是detach模式的容器,是可以看到使用-e设置的环境变量的,但是detach模式下的容器却没有生效,原因如下: exec命令其实是mydocker发起的另外一个进程,这个进程的父进程其实是宿主机的,并不是容器内的。因为在Cgo里面使用了setns 系统调用,才使得这个进程进入到了容器内的命名空间,但是由于环境变量是继承自父进程的,因此这个exec进程的环境变量其实 是继承自宿主机的,所以在exec进程内看到的环境变量其实是宿主机的环境变量。所以需要根据容器的PID获取到它的环境变量, 并赋给exec进程上。
修改"2. 实现exec命令"中的ExecContainer函数,在os.Setenv(ENV_EXEC_CMD, cmdStr)
下添加如下的代码:
|
|
getEnvsByPid函数的实现如下:
|
|
5. 实现停止容器
stop容器的原理很简单,主要就是查找到它的进程PID,然后发送SIGTERM信号,等待进程结束就好。
首先,添加一个名叫"stop"的子命令
|
|
6. 实现删除容器
mydocker rm 实现起来非常简单,主要是文件操作,因为容器对应的进程已经被停止,所以只需要将对应记录文件信息的地址删除就可以了。
首先添加一个名为"rm"的子命令
removeContainer的实现如下:
|
|
本文发表于 0001-01-01,最后修改于 0001-01-01。
本站永久域名「 jiavvc.top 」,也可搜索「 后浪笔记一零二四 」找到我。