回顾
- 【runC】01-runC-介绍与命令
- 【runC】02-runC-源码分析-[create, init, start] 前面已经介绍过容器的初始化过程,及启动流程;今天将分析runc exec 如何进入容器进程执行命令;
预热
关键技术点
-
syscall.exec
exec是操作系统的功能,该功能在现有进程的上下文中运行可执行文件,以替换先前的可执行文件。此操作也称为覆盖。尽管在其他类Unix系统中,它尤其重要。由于未创建新进程,因此进程标识符(PID)不会更改,但是进程的机器代码,数据,堆和堆栈将被新程序的机器代码,数据,堆和堆栈所替代。runc 的exec最终也是会调用该系统调用呢
命令参数
# runc exec -h
从命令参数上看,可以设置当前程序是否需要获得tty
, 是需要指定user
甚至可以设置安全相关的参数apparmor
, cap
;这个看起来和在容器建立的config.json
文件里面能指定的内容有点相似;后面深入代码看看是否进入容器和启动容器有相似之处;
源码分析
runc exec
- runc/exec.go
runc exec 的命令入口
var execCommand = cli.Command{
Name: "exec",
Usage: "execute new process inside the container",
...
Action: func(context *cli.Context) error {
...
// 执行exec 的函数入口
status, err := execProcess(context)
if err == nil {
os.Exit(status)
}
return fmt.Errorf("exec failed: %v", err)
},
SkipArgReorder: true,
}
-
runc/exec.go execProcess
这里出现了一个在前一章的容器创建过程中提到过的函数,
getContainer
该函数主要是获取一个container 的方式,是通过libcontainer.Factory
容器工厂类加载containerID 及其配置获取一个可运行的容器(里面会调用runc init
来准备容器环境)。最后也是通过前面提到的runner
进行容器的启动,但这次不同于创建容器流程,不需要进行环境大规模的配置初始化(env,cgroups,namespace,rootfs, mounts等);后面进入r.run(p)
func execProcess(context *cli.Context) (int, error) {
// 获取指定<containerID> 的容器对象
container, err := getContainer(context)
if err != nil {
return -1, err
}
...
// 获取一个符合OCI规范在容器内的 process 配置
p, err := getProcess(context, bundle)
if err != nil {
return -1, err
}
...
// runner 结构体
r := &runner{
enableSubreaper: false,
shouldDestroy: false,
container: container,
consoleSocket: context.String("console-socket"),
detach: detach,
pidFile: context.String("pid-file"),
// 动作变为run
action: CT_ACT_RUN,
// 不需要初始化
init: false,
preserveFDs: context.Int("preserve-fds"),
logLevel: logLevel,
}
// 进入该方法
return r.run(p)
}
-
runc/utils_linux.go runner.run
在上一章中也是通过这个入口进入了容器的创建流程,下面只上重点代码(需要详解请看回前一章),解析留意一下代码注释
func (r *runner) run(config *specs.Process) (int, error) {
...
// 创建一个libcontainer 的 process 结构体对象, 该对象是一个容器进程的抽象结构,主要统一配置应用
process, err := newProcess(*config, r.init, r.logLevel)
if err != nil {
return -1, err
...
tty, err := setupIO(process, rootuid, rootgid, config.Terminal, detach, r.consoleSocket)
if err != nil {
return -1, err
}
defer tty.Close()
switch r.action {
case CT_ACT_CREATE:
err = r.container.Start(process)
case CT_ACT_RESTORE:
err = r.container.Restore(process, r.criuOpts)
// 本次执行,会跑run 动作
case CT_ACT_RUN:
err = r.container.Run(process)
default:
panic("Unknown action")
}
...
}
- 下面继续看看
r.container.Run(process)
留意代码注释
func (c *linuxContainer) Run(process *Process) error {
// linux container 去启动容器的process, 进入看看
if err := c.Start(process); err != nil {
return err
}
// 这段代码是不会跑的,因为我们这次启动是不需要初始化的
if process.Init {
return c.exec()
}
return nil
}
- 下面继续看看
c.Start(process)
留意代码注释
func (c *linuxContainer) Start(process *Process) error {
...
// 这段代码是不会跑的, 同样只有在创建完整容器环境的时候才会跑
if process.Init {
if err := c.createExecFifo(); err != nil {
return err
}
}
// 进入该代码段
if err := c.start(process); err != nil {
...
return err
}
return nil
}
-
下面继续看看
c.start(process)
留意代码注释经历了几次跳转终于来到核心的调用,
c.newParentProcess
会提供一个pipe作为和子进程的通信通道,使用socketpair
建立,最后组装到runc init
的命令里面(如未指定cwd,命令会在容器的rootfs下执行), 以及返回一个setnsProcess
(核心的启动命令)。这里省略了代码展示,因为前一章已经用了大量篇幅解析了socketpair: man7.org/linux/man-p…
func (c *linuxContainer) start(process *Process) error {
// 返回一个setnsProcess
// 设置进程env _LIBCONTAINER_INITTYPE=setns
parent, err := c.newParentProcess(process)
if err != nil {
return newSystemErrorWithCause(err, "creating new parent process")
}
// 读取parent 日志文件管道
parent.forwardChildLogs()
// 启动进程
if err := parent.start(); err != nil {
return newSystemErrorWithCause(err, "starting container process")
}
...
...
return nil
}
-
下面继续看看
parent.start
cmd终于的开始执行了,执行
runc init
,init
程序会调用nsexec.c
的代码(这也是一个子进程,而且是在init进程前启动,设置ns后就会退出);通过_LIBCONTAINER_INITTYPE=setns
这个环境变量判别用什么模式进行setns,如果是standard
则是使用clone namespace 为容器建立新的namespace,这里我们是setns
所以是为容器指定了我们需要进入的进程namespace。这一步的信息传递是通过socket与nsexec.c
这个程序交互,进程间通信技术完成信息传递;容器启动的需要的namespace 数据放到setnsProcess.bootstrapData
内;
func (p *setnsProcess) start() (retErr error) {
...
// 执行命令 runc init
err := p.cmd.Start()
if err != nil {
return newSystemErrorWithCause(err, "starting setns process")
}
...
...
// 将namespace 数据传输到nsenter 上
if p.bootstrapData != nil {
if _, err := io.Copy(p.messageSockPair.parent, p.bootstrapData); err != nil {
return newSystemErrorWithCause(err, "copying bootstrap data to pipe")
}
}
// 等待exec 设置namespace 数据, 并且通过消息管道返回父进程号,然后设置process属性为runc init 的进程号,方便后面对process的操作;
if err := p.execSetns(); err != nil {
return newSystemErrorWithCause(err, "executing setns process")
}
if len(p.cgroupPaths) > 0 {
// 把runc init进程加入到, 已有容器的cgroup中
if err := cgroups.EnterPid(p.cgroupPaths, p.pid()); err != nil && !p.rootlessCgroups {
// On cgroup v2 + nesting + domain controllers, EnterPid may fail with EBUSY.
// https://github.com/opencontainers/runc/issues/2356#issuecomment-621277643
// Try to join the cgroup of InitProcessPid.
...
...
}
}
...
...
// 关闭通信的socket
if err := unix.Shutdown(int(p.messageSockPair.parent.Fd()), unix.SHUT_WR); err != nil {
return newSystemErrorWithCause(err, "calling shutdown on init pipe")
}
// 等待命令执行结束,即exec结束
if ierr != nil {
p.wait()
return ierr
}
return nil
}
至此 exec 的第一阶段完成了,下面进入第二阶段 init 过程
runc init
-
runc/libcontainer/setns_init_linux.go
我们直接看核心代码块
func (l *linuxSetnsInit) Init() error {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
...
// 省略了大量代码
...
// 最终通过exec对容器进程空间执行命令
return system.Execv(l.config.Args[0], l.config.Args[0:], os.Environ())
}
总结
-
exec 调用时序图
-
结语
终于又写完一篇分析笔记,人长大了时间就不是自己的了;时间紧......
加油吧~~~少年 !!!
On the road ~~~
点赞
|收藏