探索runC (下)

迭代极昼
• 阅读 7957

回顾

本文接 探索runC(上)

前文讲到,newParentProcess() 根据源自 config.json 的配置,最终生成变量 initProcess ,这个 initProcess 包含的信息主要有

  1. cmd 记录了要执行的可执行文件名,即 "/proc/self/exe init",注意不要和容器要执行的 sleep 5 混淆了
  2. cmd.Env 记录了名为 _LIBCONTAINER_FIFOFD=%d 记录的命名管道exec.fifo 的描述符,名为_LIBCONTAINER_INITPIPE=%d记录了创建的 SocketPairchildPipe 一端的描述符,名为_LIBCONTAINER_INITTYPE="standard"记录要创建的容器中的进程是初始进程
  3. initProcessbootstrapData 记录了新的容器要创建哪些类型的 Namespace
/* 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)

    .....
}
  1. p.cmd.Start() 启动 cmd 中设置的要执行的可执行文件 /proc/self/exe,参数是 init,这个函数会启动一个新的进程去执行该命令,并且不会阻塞。
  2. io.Copyp.bootstrapData 中的数据通过 p.parentPipe 发送给子进程

/proc/self/exe 正是runc程序自己,所以这里相当于是执行runc init,也就是说,我们输入的是runc create命令,隐含着又去创建了一个新的子进程去执行runc init。为什么要额外重新创建一个进程呢?原因是我们创建的容器很可能需要运行在一些独立的 namespace 中,比如 user namespace,这是通过 setns() 系统调用完成的,而在setns man page中写了下面一段话

A multi‐threaded process may not change user namespace with setns(). It is not permitted to use setns() to reenter the caller's current user names‐pace

即多线程的进程是不能通过 setns()改变user namespace的。而不幸的是 Go runtime 是多线程的。那怎么办呢 ?所以setns()必须要在Go runtime 启动之前就设置好,这就要用到cgo了,在Go runtime 启动前首先执行嵌入在前面的 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: -Wall
extern void nsexec();
void __attribute__((constructor)) init(void) {
    nsexec();
}
*/
import "C"

接下来,轮到 nsexec() 完成为容器创建新的 namespace 的工作了, nsexec() 同样很长,逐段来看

/* libcontainer/nsenter/nsexec.c */
void nsexec(void)
{
    int pipenum;
    jmp_buf env;
    int sync_child_pipe[2], sync_grandchild_pipe[2];
    struct nlconfig_t config = { 0 };

    /*
     * If we don't have an init pipe, just return to the go routine.
     * We'll only get an init pipe for start or exec.
     */
    pipenum = initpipe();
    if (pipenum == -1)
        return;

    /* Parse all of the netlink configuration. */
    nl_parse(pipenum, &config);
   
    ......    

上面这段 C 代码中,initpipe() 从环境中读取父进程之前设置的pipe(_LIBCONTAINER_INITPIPE记录的的文件描述符),然后调用 nl_parse 从这个管道中读取配置到变量 config ,那么谁会往这个管道写配置呢 ? 当然就是runc create父进程了。父进程通过这个pipe,将新建容器的配置发给子进程,这个过程如下图所示:

探索runC (下)

发送的具体数据在 linuxContainerbootstrapData() 函数中封装成netlink msg格式的消息。忽略大部分配置,本文重点关注namespace的配置,即要创建哪些类型的namespace,这些都是源自最初的config.json文件。

至此,子进程就从父进程处得到了namespace的配置,继续往下, nsexec() 又创建了两个socketpair,从注释中了解到,这是为了和它自己的子进程和孙进程进行通信。

void nsexec(void)
{
   .....
    /* Pipe so we can tell the child when we've finished setting up. */
    if (socketpair(AF_LOCAL, SOCK_STREAM, 0, sync_child_pipe) < 0)  //  sync_child_pipe is an out parameter
        bail("failed to setup sync pipe between parent and child");

    /*
     * We need a new socketpair to sync with grandchild so we don't have
     * race condition with child.
     */
    if (socketpair(AF_LOCAL, SOCK_STREAM, 0, sync_grandchild_pipe) < 0)
        bail("failed to setup sync pipe between parent and grandchild");
   
}

然后就该创建namespace了,看注释可知这里其实有考虑过三个方案

  1. first clone then clone
  2. first unshare then clone
  3. first clone then unshare

最终采用的是方案 3,其中缘由由于考虑因素太多,所以准备之后另写一篇文章分析

接下来就是一个大的 switch case 编写的状态机,大体结构如下,当前进程通过clone()系统调用创建子进程,子进程又通过clone()系统调用创建孙进程,而实际的创建/加入namespace是在子进程完成的

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:{
       }

本文不准备展开分析这个状态机了,而将这个状态机的流程画在了下面的时序图中,需要注意的是以下几点

  1. namespacesrunc init 2完成创建
  2. runc init 1runc init 2最终都会执行exit(0),但runc init 3不会,它会继续执行runc init命令的后半部分。因此最终只会剩下runc create进程和runc init 3进程

探索runC (下)

再回到runc create进程

func (p *initProcess) start() error {

    p.cmd.Start()
    p.process.ops = p
    io.Copy(p.parentPipe, p.bootstrapData);

    p.execSetns()
    ......

再向 runc init发送了 bootstrapData 数据后,便调用 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)

    process, err := os.FindProcess(pid.Pid)

    p.cmd.Process = process
    p.process.ops = p
    return 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 {
        switch sync.Type {
        case procReady:
            .....
            writeSync(p.parentPipe, procRun);
            sentRun = true
        case procHooks:
            .....
            // Sync with child.
            err := writeSync(p.parentPipe, procResume); 
            sentResume = true
        }

        return nil
    })
    ......

可以看到,runc create又开始通过pipe进行双向通信了,通信的对端自然就是runc init 3进程了,runc init 3进程在执行完嵌入的 C 代码后(实际是runc init 1执行的,但runc init 3也是由runc init 1间接clone()出来的),因此将开始运行 Go runtime,开始响应init命令

sleep 5 通过 p.sendConfig() 发送给了runc init进程

init命令首先通过 libcontainer.New("") 创建了一个 LinuxFactory,这个方法在上篇文章中分析过,这里不再解释。然后调用 LinuxFactoryStartInitialization() 方法。

/* libcontainer/factory_linux.go */
// StartInitialization loads a container by opening the pipe fd from the parent to read the configuration and state
// This is a low level implementation detail of the reexec and should not be consumed externally
func (l *LinuxFactory) StartInitialization() (err error) {
    var (
        pipefd, fifofd int
        envInitPipe    = os.Getenv("_LIBCONTAINER_INITPIPE")  
        envFifoFd      = os.Getenv("_LIBCONTAINER_FIFOFD")
    )

    // Get the INITPIPE.
    pipefd, err = strconv.Atoi(envInitPipe)

    var (
        pipe = os.NewFile(uintptr(pipefd), "pipe")
        it   = initType(os.Getenv("_LIBCONTAINER_INITTYPE")) // // "standard" or "setns"
    )
    
    // Only init processes have FIFOFD.
    fifofd = -1
    if it == initStandard {
        if fifofd, err = strconv.Atoi(envFifoFd); err != nil {
            return fmt.Errorf("unable to convert _LIBCONTAINER_FIFOFD=%s to int: %s", envFifoFd, err)
        }
    }

    i, err := newContainerInit(it, pipe, consoleSocket, fifofd)

    // If Init succeeds, syscall.Exec will not return, hence none of the defers will be called.
    return i.Init() //
}

StartInitialization() 方法尝试从环境中读取一系列_LIBCONTAINER_XXX变量的值,还有印象吗?这些值全是在runc create命令中打开和设置的,也就是说,runc create通过环境变量,将这些参数传给了子进程runc init 3

拿到这些环境变量后,runc init 3调用 newContainerInit 函数

/* libcontainer/init_linux.go */
func newContainerInit(t initType, pipe *os.File, consoleSocket *os.File, fifoFd int) (initer, error) {
    var config *initConfig

    /* read config from pipe (from runc process) */
    son.NewDecoder(pipe).Decode(&config); 
    populateProcessEnvironment(config.Env);
    switch t {
    ......
    case initStandard:
        return &linuxStandardInit{
            pipe:          pipe,
            consoleSocket: consoleSocket,
            parentPid:     unix.Getppid(),
            config:        config, // <=== config
            fifoFd:        fifoFd,
        }, nil
    }
    return nil, fmt.Errorf("unknown init type %q", t)
}

newContainerInit() 函数首先尝试从 pipe 读取配置存放到变量 config 中,再存储到变量 linuxStandardInit 中返回

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

回到 StartInitialization(),在得到 linuxStandardInit 后,便调用其 Init()方法了

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

    return i.Init()  
}

本文忽略掉 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

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
4年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Stella981 Stella981
4年前
Opencv中Mat矩阵相乘——点乘、dot、mul运算详解
Opencv中Mat矩阵相乘——点乘、dot、mul运算详解2016年09月02日00:00:36 \牧野(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fme.csdn.net%2Fdcrmg) 阅读数:59593
Stella981 Stella981
4年前
C# Aspose.Cells导出xlsx格式Excel,打开文件报“Excel 已完成文件级验证和修复。此工作簿的某些部分可能已被修复或丢弃”
报错信息:最近打开下载的Excel,会报如下错误。(xls格式不受影响)!(https://oscimg.oschina.net/oscnet/2b6f0c8d7f97368d095d9f0c96bcb36d410.png)!(https://oscimg.oschina.net/oscnet/fe1a8000d00cec3c
Stella981 Stella981
4年前
Linux查看GPU信息和使用情况
1、Linux查看显卡信息:lspci|grepivga2、使用nvidiaGPU可以:lspci|grepinvidia!(https://oscimg.oschina.net/oscnet/36e7c7382fa9fe49068e7e5f8825bc67a17.png)前边的序号"00:0f.0"是显卡的代
Stella981 Stella981
4年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Easter79 Easter79
4年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
4年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Python进阶者 Python进阶者
2年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这