libcontainer
已经被docker
捐给OCI
,并包装成runc
:
runc
以下内容基于runc 1.0.3
版本展开
OCI Bundle
OCI Bundle
是指满足OCI
标准的一系列文件,这些文件包含了运行容器所需要的所有数据,它们存放在一个共同的目录,该目录包含以下两项:
config.json
文件包含容器运行的配置数据root
目录为container
的root filesystem
整理流程
在runc
中所有二级命令都有单独的go
文件,通过packege cli
中的App
命令模版统一调用
创建容器
runc create -b /mycontainer test
create.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24Action: func(context *cli.Context) error {
// 入参检查
if err := checkArgs(context, 1, exactArgs); err != nil {
return err
}
//保存pid文件
if err := revisePidFile(context); err != nil {
return err
}
//加载config.json到spec数据结构
spec, err := setupSpec(context)
if err != nil {
return err
}
//创建容器
status, err := startContainer(context, spec, CT_ACT_CREATE, nil)
if err != nil {
return err
}
// exit with the container's exit status so any external supervisor is
// notified of the exit with the correct exit status.
os.Exit(status)
return nil
},utils_linux.go/createContainer/CreateLibcontainerConfig
将Spec
等相关配置信息转存到Config
数据结构,为后续工厂执行操作提供入参加载
Factory
接口,其中LinuxFactory
将作为一种实现- 创建
root
目录root
用户执行是/run/runc
- 创建
LinuxFactory
数据结构
- 创建
调用
Factory
的Create()
方法,返回Container
接口创建逻辑容器,其中linuxContainer
将作为其中一种实现- 验证容器
id
合法性 - 验证
config
合法性 - 通过
SecureJoinVFS
创建容器的root
目录${root}/containerid
- 返回
Container
对象
- 验证容器
创建
runner
在r.run(spec.Process)
中执行container.Start(process)
创建物理容器Container
主要包含了容器配置、控制等信息,是对不同操作系统下容器实现的抽象,目前在linux
平台相当于执行func (c *linuxContainer) Start(process *Process) error
- 在容器
root
目录下创建exec.fifo
- 执行内置函数
func (c *linuxContainer) start(process *Process) (retErr error)
- 创建
parentProcess
,参与物理容器创建过程的Process
一共有两个实例第一个叫
Process
,用于物理容器内进程的配置和IO
的管理,前面在runner run
创建的Process
就是指它,该实例主要来自spec.Process
另一个叫
ParentProcess
,负责从物理容器外部处理物理容器启动工作,与Container
对象直接进行交互。启动工作完成后,ParentProcess
负责执行等待、发信号、获得容器内进程pid
等管理工作,initProcess
是ParentProcess
的一个实现1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38type parentProcess interface {
// pid returns the pid for the running process.
pid() int
// start starts the process execution.
start() error
// send a SIGKILL to the process and wait for the exit.
terminate() error
// wait waits on the process returning the process state.
wait() (*os.ProcessState, error)
// startTime returns the process start time.
startTime() (uint64, error)
signal(os.Signal) error
externalDescriptors() []string
setExternalDescriptors(fds []string)
forwardChildLogs() chan error
}
type initProcess struct {
cmd *exec.Cmd
messageSockPair filePair
logFilePair filePair
config *initConfig
manager cgroups.Manager
intelRdtManager intelrdt.Manager
container *linuxContainer
fds []string
process *Process
bootstrapData io.Reader
sharePidns bool
}
- 调用
parentProcess
的forwardChildLogs
通过管道不断获取容器内的日志 - 调用
parentProcess
的start
,其实就是执行func (p *initProcess) start() (retErr error)
- 执行
err := p.cmd.Start()
会执行runc init
命令,但是这个子进程会阻塞在nsenter
的func init()
,直到通过nsenter
让父进程把需要用到相关ns
的信息写入socketpair
后才执行后面的函数1
2
3
4if _, err := io.Copy(p.messageSockPair.parent, p.bootstrapData); err != nil {
return newSystemErrorWithCause(err, "copying bootstrap data to pipe")
}
err = <-waitInit - 向子进程发送
initConfig
,init
进行容器初始化,创建网络、路由表、rootfs
等 - 通过
messageSockPair.parent
进行一系列消息交互,init
向父进程发送procReady
,等待父进程发送procRun
,代表init
可以进行execv
- 最后
init
会以只写方式打开exec.fifo
,这里会阻塞直到另一个进程以读方式打开管道。然后写一个字符0
,去执行后续的execv
1
2
3
4
5
6
7
8fd, err := unix.Open("/proc/self/fd/"+strconv.Itoa(l.fifoFd), unix.O_WRONLY|unix.O_CLOEXEC, 0)
if err != nil {
return newSystemErrorWithCause(err, "open exec fifo")
}
if _, err := unix.Write(fd, []byte("0")); err != nil {
return newSystemErrorWithCause(err, "write 0 exec fifo")
}
- 执行
- 创建
- 在容器
最终
runc init
进程将分离并作为容器内的init
一直执行,容器的状态变为created
容器的状态会保存在
/run/runc/${containerid}/state.json
运行容器
runc start test
- 基于
/run/runc/${containerid}/state.json
加载container
结构 start
就是执行container
的exec
接口,将对应容器的exec.fifo
以读方式打开,这样init
就不会阻塞,去执行exec
的命令,后面使用select
读取fifo
内容(字符0
)- 读取到字符
0
后handleFifoResult(result)
将删除exec.fifo
文件
1 | func (c *linuxContainer) exec() error { |
runc exec
exec
时创建的parentProcess
对应实例就不是initProcess
而是setnsProcess
,并且在执行data, err := c.bootstrapData(0, state.NamespacePaths)
时不需要任何cloneFlags
,只需要setns
相关进程对应ns
路径
然后执行setnsProcess
的start()
,去执行/proc/self/exe init
, 将在main
之前调用nsexec.c
的nsexec
去setns
执行完main
之前命名空间切换操作将来到setns_init_linux.go
中的Init
,根据父进程传入的initConfig
执行容器tty
、能力、Seccomp
等配置 ,最后执行execv