关注分享主机优惠活动
国内外VPS云服务器

探索 runC(第 2 部分)

摘要:不幸的是,这是多线程的。 此时,子进程从父进程检索配置并继续创建另外两个配置。 据我从评论中了解到,这是与自己的子进程和孙进程进行通信。

回顾

本文继续讨论runC(第1部分)

如上所述, newParentProcess() 最终根据config.json中的设置生成变量initProcess。 这个initProcess中包含的信息主要包括

cmd记录。 这记录了运行的可执行文件的名称,即“/proc./self/exe init”。 注意不要与容器运行的Sleep 5名称相匹配。 > 混淆

cmd.Env记录exec.fifo命名管道_LIBCONTAINER_FIFOFD=%d命名的描述符_LIBCONTAINER_INITPIPE=%d 记录创建的套接字对子管道端的描述符。 名称为_LIBCONTAINER_INITTYPE="标准"。 容器中创建的进程就是初始进程。

initProcessbootstrapData记录了正在创建的新容器命名空间的类型。

/* libcontainer/container_linux.go */func (c *linuxContainer) start(process *Process) error {parent, err := c.newParentProcess(process) /* 1.创建parentProcess(已完成) */ err :=parent.start() /* 2. 启动这个parentProcess */……

一旦前期工作完成,就必须通过调用start()方法来启动它。

注意:sleep 5 提示现在存储在变量 parent 中。  

runC create实现原理(二)

start()函数太长,我们一段一段看

/* libcontainer/process_linux.go */func (p *initProcess) start() error { p.cmd. Start() p.process.ops = p io.Copy(p.parentPipe, p.bootstrapData) . ....}

p.cmd.Start() 启动cmd配置的可执行文件/proc/self/exe设置为可执行。 该参数是初始化。 该函数启动一个新进程。 执行命令而不阻塞。

io.Copyp.bootstrapData数据通过p.parentPipe

/proc/self/exe发送到子进程。 这正是 runc 程序本身。 换句话说,这与运行 runc init 相同。 换句话说,输入 runc create 命令会隐式创建一个新的子进程。 。 运行runc init。 为什么需要重新创建额外的进程?原因是您创建的容器可能会在单独的命名空间中运行,例如用户命名空间,因为这是必要的。 。 这是通过 setns() 系统调用完成,我在 setns 手册页上写了以下段落。

多线程进程可能无法使用 setns() 更改用户命名空间。   不允许使用 setns() 重新进入调用者当前的用户命名空间。  

这意味着多线程进程无法通过setns 更改用户命名空间。 ()不幸的是,Go运行时是多线程的,因此您需要配置Go运行时setns()。 /strong> 已启动。 这是必需的。 在 Go 运行时启动之前首先运行嵌入式 C 代码。

的 nsenter README 中提供了具体说明。 >runc init 命令。 响应位于文件 init.go, nsenter 导入包

/* init.go */import ( "os" "runtime" "github.com/opencontainers/runc/libcontainer" _ "github . com/opencontainers/runc/libcontainer/nsenter" "github.com/urfave/cli")

nsenter 包的顶部通过 cgo 调用一段C代码nsexec()

package nsenter/*/* nsenter.go */#cgo CFLAGS: -Wallextern void nsexec( ); void __attribute__( (constructor)) init(void) { nsexec();}*/import "C"

现在轮到 nsexec() 了。 新的命名空间已创建。 nsexec()也比较长,我们来一一看一下。

/* libcontainer/nsenter /nsexec.c */void nsexec(void ){ int Pipenum; intsync_child_pipe[2],sync_grandchild_pipe[2]; * * 如果你没有 init 管道,只需 retu继续执行 go 例程。   * 仅获取用于启动或运行的初始化管道。  */ Pipenum = initpipe(); if (pipenum == -1) return; /* 解析所有 netlink 配置。  * / nl_parse(pipenum, &config ); ...

在上面的C代码中,initpipe()在父进程之前调用环境>管道。读取设置为 的 <b 值。 由 _LIBCONTAINER_INITPIPE 记录的描述符),然后调用 nl_parse 将此管道中的配置读取到变量 config 中。 那么谁来编写配置呢?这个管道是 runc create 父进程。 父进程通过这个管道将新容器的配置发送给子进程。 如下图:

发送的具体数据是封装在linuxContainerbootstrapData()函数中的。 >netlink msg 格式消息。 忽略大部分配置,本文重点关注命名空间配置:创建什么样的命名空间

此时,子进程已经从父进程获取了命名空间设置。 接下来,我们还创建nsexec()。创建了两个套接字对。 从评论中我了解到这是为了与子进程和孙进程进行通信。

void nsexec(void){ ..... /* 管道,以便您可以告诉子进程设置已完成。  */ if (socketpair(AF_LOCAL, SOCK_STREAM, 0,sync_child_pipe) < 0) //sync_child_pipe 是一个输出参数。  bail("无法在父级和子级之间建立同步管道"); /* * 需要一个新的套接字对来与孙级同步,以避免与子级的竞争条件。   */ if (socketpair(AF_LOCAL, SOCK_STREAM, 0,sync_grandchild_pipe) < 0) bail("无法在父级和孙级之间设置同步管道") }

然后创建命名空间. 如果您查看评论,您会发现这里实际上考虑了三个选项。

先克隆,再克隆。 克隆

先取消共享,再克隆

先克隆,再取消共享

最终采用方案3。 需要考虑的因素太多了,我先准备一下,然后再写。文章分析

下一步是通过switch case创建一个大型状态机。 总体结构是: 当前进程通过clone()。 b>系统调用创建子进程,子进程通过clone()系统调用创建孙进程。 命名空间创建/加入在子进程中完成 p>

switch (setjmp(env)) { case JUMP_PARENT:{ ..... clone_parent(&env, JUMP_CHILD) ; ..... } case JUMP_CHILD:{ ... if (config.namespaces ) join_namespaces(config.namespaces);clone_parent(&env, JUMP_INIT); ...... } case JUMP_INIT:{ }

本文无意分析这个状态机,但其过程如下面的序列图所示。 应注意以下几点:

命名空间是使用runc init 2

runc init 1 和 runc init 2 最终会执行 exit(0),但 runc init 3 不会。 是的,继续第二次运行。 runc init 命令的一半。 因此,最终只剩下 runc createrunc init 3 进程。

返回runc createProcess

func (p *initProcess) start() error { p.cmd.Start() p.process.ops = p io.Copy(p.parentPipe, p .bootstrapData); p.execSetns()....

bootstrapData 将数据发送到 runc init 后,execSetns() 调用。 > 等待runc init 1进程退出,从管道中获取runc init 3进程的pid,并传输该进程。 已保存 p.process.ops

/* libcontainer/process_linux.go */func (p *initProcess) execSetns() error { status, err := p.cmd Process. .Wait() var pid *pid json.NewDecoder(p.parentPipe).Decode(&pid) 进程,错误 := os.FindProcess(pid.Pid) p.cmd.Process = 进程 p.process.ops = p 返回 nil }

继续start()

func (p *initProcess) start() error { ...... p.execSetns() fds ,err := getPipeFds(p.pid()) p.setExternalDescriptors(fds) p.createNetworkInterfaces() p.sendConfig() parseSync(p.parentPipe, func(sync *syncT) error { switchsync.Type { case procReady: . .... writeSync(p.parentPipe, procRun); sendRun = true case procHooks: ..... // 与子进程同步 := writeSync(p.parentPipe, procResume); return nil }) .... .. 

可以看到,runc create再次通过pipe开始双向通信,当然通信的另一端是 runc init 3进程,即运行嵌入式C代码后的runc init 3进程(实际上是runc init 1运行但是,由于runc init 3也是从runc init 1间接得到的clone()Go运行时强>执行开始并响应init命令

通过 p.sendConfig() 睡眠 5。发送到 runc init 进程。  

init命令首先通过libcontainer.New("")创建。 至于>LinuxFactory,这个方法在之前的文章中已经分析过,这里不再讨论。 接下来,调用LinuxFactoryStartInitialization()方法。

/* libcontainer/factory_linux.go */// StartInitialization 通过从父级打开管道 fd 并读取配置和状态来加载容器 // 这是重新运行细节的低级实现,应该如下:   externallyfunc (l *LinuxFactory) StartInitialization() (err error) { var ( Pipefd, fifofd int envInitPipe = os.Getenv("_LIBCONTAINER_INITPIPE") envFifoFd = os.Getenv("_LIBCONTAINER_FIFOFD") ) // 获取 INITPIPE。    Pipefd, err = strconv.Atoi(envInitPipe) var (pipe = os.NewFile(u)intptr(pipefd), "pipe") it = initType(os.Getenv("_LIBCONTAINER_INITTYPE")) // // "standard" 或 "setns" ) // 只有 init 进程有 FIFOFD。   fifofd = -1 if it == initStandard { if fifofd, err = strconv.Atoi(envFifoFd); err != nil { return fmt.Errorf("_LIBCONTAINER_FIFOFD=无法将 %s 转换为 int: %s", envFifoFd , err) } } i, err := newContainerInit(it, Pipe, consoleSocket, fifofd) // 如果 Init 成功,syscall.Exec 不会返回,因此不会调用任何延迟。   return i.Init() //}

StartInitialization() 方法尝试从环境中读取一组 _LIBCONTAINER_XXX 变量值。 你有什么想法吗? 所有这些值都是使用 runc create 命令打开和设置的。 也就是说,runc create通过环境变量将这些参数传递给子进程runc。 init 3

获取这些环境变量后,runc init 3调用new。ContainerInit函数

/* libcontainer/init_linux.go */func newContainerInit(t initType, Pipe *os.File, consoleSocket *os.File, fifoFd int) (initer, error) { var config *initConfig /* 从管道读取配置(从 runc 进程) */ Son.NewDecoder(pipe).Decode(&config); switch t { ...... case initStandard: return &linuxStandardInit{ Pipe: Pipe, consoleSocket: consoleSocket,parentPid: unix.Getppid(), config: config, // <=== config fifoFd: fifoFd, }, nil } return nil, fmt.Errorf("未知的初始化类型 %q ", t)}

newContainerInit() 函数首先尝试从管道读取配置。 b> 在变量config 和变量li 中。使用 nuxStandardInit 返回

 runc create runc init 3 | | p.sendConfig() --- config --> NewContainerInit()
sleep 五个线索在 initStandard.config

运行 StartInitialization() 并获取 linuxStandardInit 调用后返回。 init()方法

/* init.go */.   func (l *LinuxFactory) StartInitialization() (err error) { ...... i, err := newContainerInit(it , Pipe, consoleSocket, fifofd) return i.Init() }

This在本文中,我们将忽略 Init() 方法之前的许多其他设置,只看最后。

func (l *linuxStandardInit) Init() error { .... .. name, err := exec.LookPath(l.config.Args[0]) syscall.Exec(name, l. config.Args[0:], os.Environ())}

可以看到,用户最初设置的sleep 5最终在这里被执行。

未经允许不得转载:主机频道 » 探索 runC(第 2 部分)

评论 抢沙发

评论前必须登录!

 

本站不销售/不代购主机产品,不提供技术支持,仅出于个人爱好分享优惠信息,请遵纪守法文明上网

Copyright © 主机频道 - ZHUJIPINDAO.COM ,本站托管于国外主机商

© 2021-2024   主机频道   网站地图 琼ICP备2022006744号