runc 内部实现深入解读
最近把runc的实现代码仔细的看了一遍,有点复杂,特别是nsenter那块反反复复看了不下三遍才搞懂是个什么机制,不过确实写得不错,防止日后遗忘,另外也给其他朋友一点参考(网上有一些关于runc的介绍文章,但是没有nsenter这块的介绍)
概要设计
runc的实现分为两个大阶段:
- 第一个阶段叫bootstrap, 设置一些环境以及配置信息, 创建匿名管道, 把容器的配置信息通过管道发送给第二阶段。
- 第二个阶段代码里没有明确的名称,就把它叫做worker阶段吧, 主要就是创建子进程并根据管道传过来的配置信息,设置进程的namespace信息。
bootstrap和worker主要通过socketpair这种全双工管道交互,这么实现的主要考虑是worker的大部分时间处于自己的namespace中,所以有些事情自己做不了,必须让bootstrap协助完成。
另外bootstrap用GO语言实现,worker的开始的最重要的部分用C语言实现,后面的部分也是以GO语言实现,中间的切换非常美妙。
另外值得说一下的是runc支持linux,windows,solaris等多种操作系统,所以代码的一个通用规则是对于某个功能是定一个一个接口,然后针对各种平台的实现定义一个独立的文件,比如
container.go
container_linux.go
container_widnows.go
container_solaris.go
这样可以方便理解实现,也不要被代码量吓倒。
bootstrap代码分析
下面结合runc create <nn>
来一起看一下runc具体执行流程:
create.go:
spec, err := setupSpec(context)
进入到中执行,先解析config.json文件,下载bundle,然后
status, err := startContainer(context, spec, CT_ACT_CREATE, nil)
开始了utils_linux.go,
func startContainer(context *cli.Context, spec *specs.Spec, action CtAct, criuOpts *libcontainer.CriuOpts) (int, error) {
container, err := createContainer(context, id, spec)
.....
r := &runner{
enableSubreaper: !context.Bool("no-subreaper"),
shouldDestroy: true,
container: container,
listenFDs: listenFDs,
notifySocket: notifySocket,
consoleSocket: context.String("console-socket"),
detach: context.Bool("detach"),
pidFile: context.String("pid-file"),
preserveFDs: context.Int("preserve-fds"),
action: action,
criuOpts: criuOpts,
}
return r.run(spec.Process)
}
看一下createContainer的实现
func createContainer(context *cli.Context, id string, spec *specs.Spec) (libcontainer.Container, error) {
....
factory, err := loadFactory(context)
return factory.Create(id, config)
}
loadFactory最重会走到factory_linux.go
func New(root string, options ...func(*LinuxFactory) error) (Factory, error) {
if root != "" {
if err := os.MkdirAll(root, 0700); err != nil {
return nil, newGenericError(err, SystemError)
}
}
l := &LinuxFactory{
Root: root,
InitArgs: []string{"/proc/self/exe", "init"},
Validator: validate.New(),
CriuPath: "criu",
}
Cgroupfs(l)
for _, opt := range options {
if err := opt(l); err != nil {
return nil, err
}
}
return l, nil
}
到目前为止都很简单,两个值得注意下的
InitArgs: []string{"/proc/self/exe", “init”}, /proc/self/exe
是指向自身,也就是runc
,这个很重要,后面会涉及到
options里面定义了具体采用哪种方式的cgroups,给予文件的还是systemd的方式
好了,有了factory,那就用这个factory创建container结构体,
func (l *LinuxFactory) Create(id string, config *configs.Config) (Container, error) {
containerRoot := filepath.Join(l.Root, id)
if err := os.MkdirAll(containerRoot, 0711); err != nil {
return nil, newGenericError(err, SystemError)
}
if err := os.Chown(containerRoot, unix.Geteuid(), unix.Getegid()); err != nil {
return nil, newGenericError(err, SystemError)
}
if config.Rootless {
RootlessCgroups(l)
}
c := &linuxContainer{
id: id,
root: containerRoot,
config: config,
initArgs: l.InitArgs,
criuPath: l.CriuPath,
cgroupManager: l.NewCgroupsManager(config.Cgroups, nil),
}
c.state = &stoppedState{c: c}
return c, nil
}
看到创建了一个linuxContainer结构体,并设置状态为Stopped, 最后开始run这个container,注意这个run不适真正的run,只是初始化container的许多信息而已。
func (r *runner) run(config *specs.Process) (int, error) {
process, err := newProcess(*config)
if len(r.listenFDs) > 0 {
process.Env = append(process.Env, fmt.Sprintf("LISTEN_FDS=%d", len(r.listenFDs)), "LISTEN_PID=1")
process.ExtraFiles = append(process.ExtraFiles, r.listenFDs...)
}
baseFd := 3 + len(process.ExtraFiles)
for i := baseFd; i < baseFd+r.preserveFDs; i++ {
process.ExtraFiles = append(process.ExtraFiles, os.NewFile(uintptr(i), "PreserveFD:"+strconv.Itoa(i)))
}
...
switch r.action {
case CT_ACT_CREATE:
err = r.container.Start(process)
case CT_ACT_RESTORE:
err = r.container.Restore(process, r.criuOpts)
case CT_ACT_RUN:
err = r.container.Run(process)
default:
panic("Unknown action")
}
最重进入container_linux.go : linuxContainer.Start
func (c *linuxContainer) start(process *Process, isInit bool) error {
parent, err := c.newParentProcess(process, isInit)
if err != nil {
return newSystemErrorWithCause(err, "creating new parent process")
}
if err := parent.start(); err != nil {
// terminate the process to ensure that it properly is reaped.
if err := parent.terminate(); err != nil {
logrus.Warn(err)
}
return newSystemErrorWithCause(err, "starting container process")
}
...
func (c *linuxContainer) newParentProcess(p *Process, doInit bool) (parentProcess, error) {
parentPipe, childPipe, err := utils.NewSockPair("init")
if err != nil {
return nil, newSystemErrorWithCause(err, "creating new init pipe")
}
cmd, err := c.commandTemplate(p, childPipe)
if err != nil {
return nil, newSystemErrorWithCause(err, "creating new command template")
}
if !doInit {
return c.newSetnsProcess(p, cmd, parentPipe, childPipe)
}
// We only set up fifoFd if we're not doing a `runc exec`. The historic
// reason for this is that previously we would pass a dirfd that allowed
// for container rootfs escape (and not doing it in `runc exec` avoided
// that problem), but we no longer do that. However, there's no need to do
// this for `runc exec` so we just keep it this way to be safe.
if err := c.includeExecFifo(cmd); err != nil {
return nil, newSystemErrorWithCause(err, "including execfifo in cmd.Exec setup")
}
return c.newInitProcess(p, cmd, parentPipe, childPipe)
}
主要做了三件事:
- 创建SockPair管道
- 创建cmd, 并把管道的一端给cmd
- 根据状态创建initProcess 或者 setnsProcess
侧重看一下cmd:
func (c *linuxContainer) commandTemplate(p *Process, childPipe *os.File) (*exec.Cmd, error) {
cmd := exec.Command(c.initArgs[0], c.initArgs[1:]...)
cmd.Stdin = p.Stdin
cmd.Stdout = p.Stdout
cmd.Stderr = p.Stderr
cmd.Dir = c.config.Rootfs
if cmd.SysProc