鳩小屋

落書き帳

Low-level Container Runtime:Runc Internals

低レベルコンテナランタイムruncの内部処理のまとめです。

f:id:FallenPigeon:20210502181030p:plain

参考

2021/05現在:Container Runtime Meetupのrunc概説と大きな相違はありませんでした。
短時間で要点だけ抑えたい方は、下記資料を参照した方がよいかもしれません。
medium.com

runcの理解には、コンテナ技術やGo言語の基礎知識が必要です。
今回はDockerの操作程度では飽き足りないというような上級者向けの内容となっています。
自分の理解も大概怪しいので多少間違ってても許してください(´・ω・`)
github.com

おさらい

#OCIランタイムバンドル
$ ls ubuntu-bundle/
config.json rootfs
#コンテナ生成
$runc run --bundle ubuntu-bundle ubuntu-container
root@umoci-default:/#
/*config.json*/
{
    "process": {
        "terminal":false,
        "user": {
            "uid": 0,
            "gid": 0
        },
        "args": [
            "sh"
        ],
        "env": [
            "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
            "TERM=xterm"  
        ],
        "cwd": "/",
    },
    "root": {
        "path": "rootfs",
        "readonly": true
    },   
    "linux": {
        "namespaces": [
            {
                "type": "pid"
            },
            {
                "type": "network"
            },
            {
                "type": "ipc"
            },
            {
                "type": "uts"
            },
            {
                "type": "mount"
            }
        ],
    }
}

runc architecture

file

runcは主に「各コマンドに対応するルートディレクトリのgoファイル」と「直接コンテナを管理/起動/作成するためlibcontainer」で構成されています。
runcが実行されると、main.goから引数に対応するコマンドが実行されます。各コマンドは、libcontainer経由でlinuxの機能にアクセスして、コンテナを制御します。
f:id:FallenPigeon:20210430085924p:plain

runc
├── main.go
├── create.go
├── delete.go
├── events.go
├── exec.go
├── init.go
├── kill.go
├── man
├── notify_socket.go
├── pause.go
├── ps.go
├── restore.go
├── rlimit_linux.go
├── rootless_linux.go
├── run.go
├── signals.go
├── spec.go
├── start.go
├── state.go
├── tty.go
├── update.go
├── utils.go
├── utils_linux.go
├── libcontainer
│   ├── README.md
│   ├── SPEC.md
│   ├── apparmor
│   ├── capabilities
│   ├── cgroups
│   ├── configs
│   ├── console_linux.go
│   ├── container.go
│   ├── container_linux.go
│   ├── container_linux_test.go
│   ├── criu_opts_linux.go
│   ├── devices
│   ├── error.go
│   ├── error_test.go
│   ├── factory.go
│   ├── factory_linux.go
│   ├── factory_linux_test.go
│   ├── generic_error.go
│   ├── generic_error_test.go
│   ├── init_linux.go
│   ├── integration
│   ├── intelrdt
│   ├── keys
│   ├── logs
│   ├── message_linux.go
│   ├── network_linux.go
│   ├── notify_linux.go
│   ├── notify_linux_test.go
│   ├── notify_linux_v2.go
│   ├── nsenter
│   ├── process.go
│   ├── process_linux.go
│   ├── restored_process.go
│   ├── rootfs_linux.go
│   ├── rootfs_linux_test.go
│   ├── seccomp
│   ├── setns_init_linux.go
│   ├── specconv
│   ├── stacktrace
│   ├── standard_init_linux.go
│   ├── state_linux.go
│   ├── state_linux_test.go
│   ├── stats_linux.go
│   ├── sync.go
│   ├── system
│   ├── user
│   ├── userns
│   └── utils
main.go and command

runcのコマンドはGoのCLIツールとして実装されていて、cli.Commandのところでコマンド一覧が定義されています。また、app.Run(os.Args)で指定されたサブコマンドを実行しています。

/* main.go */
func main() {
        app := cli.NewApp()
        app.Name = "runc"
        app.Usage = usage
...
        app.Commands = []cli.Command{
        checkpointCommand,
        createCommand,
        deleteCommand,
        eventsCommand,
        execCommand,
        initCommand,
        killCommand,
        listCommand,
        pauseCommand,
        psCommand,
        restoreCommand,
        resumeCommand,
        runCommand,
        specCommand,
        startCommand,
        stateCommand,
        updateCommand,
        }
...
        if err := app.Run(os.Args); err != nil {
                fatal(err)
        }

cli.Commandに指定されたコマンドは、ルートディレクトリの各goファイルに定義されています。 createCommandを例に挙げると、create.goに定義されていて、サブコマンド名はcreateとなっています。

runc createを実行すると、create.goのAction: func(context *cli.Context)が実行されます。

/*create.go*/

var createCommand = cli.Command{
        Name: "create",
        Usage: "create a container",
        ArgsUsage: `

...
        Action: func(context *cli.Context) error {
                if err := checkArgs(context, 1, exactArgs); err != nil {
                        return err
                }
                if err := revisePidFile(context); err != nil {
                        return err
                }
                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
        },
}

process

runcをプロセス構成の視点で見ると、
まず、runc createコマンドでコンテナの管理オブジェクトなどが作成されます。
次に、runc initでnamespaceなどの具体的な初期化が行われてコンテナが作成されます。
特にrunc initは複雑なので管理データや同期処理に注意してください。

f:id:FallenPigeon:20210502181030p:plain

以降、runc create→runc init→runc startの順でruncの処理を追います。
同期処理の都合で、説明が前後するところもありますが、上記フローを眺めながら、コードを確認するとよいと思います。

runc create

createコマンドの処理で重要なのはsetupSpecとstartContainerのところです。

/*create.go*/
        Action: func(context *cli.Context) error {
...
                spec, err := setupSpec(context)
...
                status, err := startContainer(context, spec, CT_ACT_CREATE, nil)
...

以降、下記の関数階層を辿りながらcreateコマンドの処理を追います。

 setupSpec(context)
 startContainer(context, spec, CT_ACT_CREATE, nil) 
   |- createContainer
      |- specconv.CreateLibcontainerConfig
      |- loadFactory(context)
         |- libcontainer.New(......)
      |- factory.Create(id, config)
   |- runner.run(spec.Process)
      |- newProcess(*config, r.init) 
      |- r.container.Start(process)
         |- c.createExecFifo()
         |- c.start(process)
            |- c.newParentProcess(process)
            |- parent.start()

setupSpec

setupSpecメソッドは、ユーザに指定されたランタイムバンドルを参照して、設定情報をrunc用のデータ構造に格納します。
1.コマンドライン入力から-bで指定されたOCIバンドルディレクトリを参照します。引数がない場合、デフォルトは現在のディレクトリを参照します。
2.config.jsonを読み取り、設定をhttps://github.com/opencontainers/runtime-spec/blob/master/specs-go/config.goで定義されているGoデータ構造specs.Specに変換します。内容はすべてOCI準拠です。

startContainer

Linuxプラットフォームを使用しているため、実際の呼び出しはutils_linux.goのstartContainer()になります。
startContainerメソッドは、linuxContainerと呼ばれるコンテナの雛形やコンテナの作成を担うLinuxFactoryをインスタンスとして生成します。

3番目の引数はCT_ACT_CREATEとなっていますが、これはコンテナが作成されるだけということを意味します。 startContainerメソッドは、createContainerメソッドを呼び出して、linuxContainerオブジェクト(コンテナの雛形)を作成し、runner.runメソッドを介してコンテナを開始します。

/* utils_linux.go */
func startContainer(context *cli.Context, spec *specs.Spec, action CtAct, criuOpts *libcontainer.CriuOpts) (int, error) {
	id := context.Args().First()
...
	//specからruncのコンテナ構成(linuxContainer)を作成
	container, err := createContainer(context, id, spec)
...
	r := &runner{
		enableSubreaper: !context.Bool("no-subreaper"),
		shouldDestroy:   true,
		container:       container,//linuxContainer
		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,
		init:            true,//process.Initフィールドを設定するために使用
		logLevel:        logLevel,
	}
	return r.run(spec.Process)
}

createContainerメソッドやrunner.runメソッドを説明する前にlinuxContainerと呼ばれるコンテナの雛形やコンテナの作成を担うLinuxFactoryを説明します。

linuxContainer

runCには、Containerという抽象インターフェイスがコンテナオブジェクトを表すために存在します。これにはBaseContainerインターフェイスが含まれています。内部メソッド名を確認するとコンテナ管理の処理が定義されていることが分かります。
linuxContainerが抽象インターフェースの具体的な実装です。以下はその定義であり、initPathが重要です。

/* libcontainer/container.go */
type BaseContainer interface {
	ID() string
	Status() (Status, error)
	State() (*State, error)
	Config() configs.Config
	Processes() (int, error)
	Stats() (*Stats, error)
	Set(config configs.Config) error
	Start(process *Process) (err error)
	Run(process *Process) (err error)
	Destroy() error
	Signal(s os.Signal, all bool) error
	Exec() error
}

/* libcontainer/container_linux.go */
type Container interface {
	BaseContainer

	Checkpoint(criuOpts *CriuOpts) error
	Restore(process *Process, criuOpts *CriuOpts) error
	Pause() error
	Resume() error
	NotifyOOM() (<-chan struct{}, error)
	NotifyMemoryPressure(level PressureLevel) (<-chan struct{}, error)
}

type linuxContainer struct {
	id                   string
	root                 string
	config               *configs.Config
	cgroupManager        cgroups.Manager
	intelRdtManager      intelrdt.Manager
	initPath             string
	initArgs             string
	initProcess          parentProcess
	initProcessStartTime uint64
	criuPath             string
	newuidmapPath        string
	newgidmapPath        string
	m                    sync.Mutex
	criuVersion          int
	state                containerState
	created              time.Time
	fifo                 *os.File
}
LinuxFactory

runCでは、すべてのコンテナはコンテナファクトリによって作成されます。ファクトリは次のように定義された抽象インターフェイスであり、4つのメソッドが含まれています。また、LinuxFactoryが抽象インターフェースの具体的な実装です。

/*libcontainer/factory.go*/
type Factory interface {
	Create(id string, config *configs.Config) (Container, error)
	Load(id string) (Container, error)
	StartInitialization() error
	Type() string
}

/*libcontainer/factory_linux.go*/
type LinuxFactory struct {
	// Root directory for the factory to store state.
	Root string

	// InitPath is the path for calling the init responsibilities for spawning
	// a container.
	InitPath string

	// InitArgs are arguments for calling the init responsibilities for spawning
	// a container.
	InitArgs []string

	// CriuPath is the path to the criu binary used for checkpoint and restore of
	// containers.
	CriuPath string

	// New{u,g}idmapPath is the path to the binaries used for mapping with
	// rootless containers.
	NewuidmapPath string
	NewgidmapPath string

	// Validator provides validation to container configurations.
	Validator validate.Validator

	// NewCgroupsManager returns an initialized cgroups manager for a single container.
	NewCgroupsManager func(config *configs.Cgroup, paths map[string]string) cgroups.Manager

	// NewIntelRdtManager returns an initialized Intel RDT manager for a single container.
	NewIntelRdtManager func(config *configs.Config, id string, path string) intelrdt.Manager
}

createContainer

startContainerメソッドから呼び出されたcreateContainerメソッドは、Config.config型のconfigにLibcontainer構成情報を格納します。
次にcontextをロードしてLinuxFactoryを生成します。
最後にLinuxFactoryは、CreateメソッドでlinuxContainerを生成します。

func createContainer(context *cli.Context, id string, spec *specs.Spec) (libcontainer.Container, error) {
	rootlessCg, err := shouldUseRootlessCgroupManager(context)
	if err != nil {
		return nil, err
	}
        //OCI仕様に従ってLibcontainerのconfigを作成
	config, err := specconv.CreateLibcontainerConfig(&specconv.CreateOpts{
		CgroupName:                id,
		UseSystemdCgroup:      context.GlobalBool("systemd-cgroup"),
		NoPivotRoot:                 context.Bool("no-pivot"),
		NoNewKeyring:             context.Bool("no-new-keyring"),
		Spec:                              spec,
		RootlessEUID:                os.Geteuid() != 0,
		RootlessCgroups:          rootlessCg,
	})
	if err != nil {
		return nil, err
	}
        //LinuxFactoryを生成
	factory, err := loadFactory(context)
	if err != nil {
		return nil, err
	}
        //FactoryのCreateメソッドでlinuxContainerを生成
	return factory.Create(id, config)
}
CreateLibcontainerConfig

Libcontainerのconfigの作成手順を確認します。

/*libcontainer/specconv/spec_linux.go*/
func CreateLibcontainerConfig(opts *CreateOpts) (*configs.Config, error) {
       // runcの作業ディレクトリを、ランタイムバンドルがあるカレントディレクトリに設定。
	rcwd, err := os.Getwd()
	if err != nil {
		return nil, err
	}
	...
	// config.jsonのrootfsディレクトリを設定。
	rootfsPath := spec.Root.Path
	if !filepath.IsAbs(rootfsPath) {
		rootfsPath = filepath.Join(cwd, rootfsPath)
	}
	labels := string{}
	for k, v := range spec.Annotations {
		labels = append(labels, k+"="+v)
	}
	// 既存のcreateOptsを整理
	config := &configs.Config{
		Rootfs:          rootfsPath,
		NoPivotRoot:     opts.NoPivotRoot,
		Readonlyfs:      spec.Root.Readonly,
		Hostname:        spec.Hostname,
		Labels:          append(labels, "bundle="+cwd),
		NoNewKeyring:    opts.NoNewKeyring,
		RootlessEUID:    opts.RootlessEUID,
		RootlessCgroups: opts.RootlessCgroups,
	}
 	// config.jsonのmountsフィールドに対応する、仕様に従ってディレクトリをマウント。
        // /Proc、/dev、/dev/pts、/dev/shm、/dev/mqueue、/sys/、/sys/fs/cgroupなど
	for _, m := range spec.Mounts {
		config.Mounts = append(config.Mounts, createLibcontainerMount(cwd, m))
	}

	// マウント・パーティション、デフォルト・マウント・パーティション AllowedDevices、OCI準拠パーティションの作成
	// AllowedDevices https://github.com/opencontainers/runc/blob/master/libcontainer/specconv/spec_linux.go
	defaultDevs, err := createDevices(spec, config)
	if err != nil {
		return nil, err
	}

	legacySubsystems = subsystem{
		&fs.CpusetGroup{},
		&fs.DevicesGroup{},
		&fs.MemoryGroup{},
		&fs.CpuGroup{},
		&fs.CpuacctGroup{},
		&fs.PidsGroup{},
		&fs.BlkioGroup{},
		&fs.HugetlbGroup{},
		&fs.PerfEventGroup{},
		&fs.FreezerGroup{},
		&fs.NetPrioGroup{},
		&fs.NetClsGroup{},
		&fs.NameGroup{GroupName: "name=systemd"},
	}
	// cgroup構成を作成。
	c, err := CreateCgroupConfig(opts, defaultDevs)
	if err != nil {
		return nil, err
	}

	config.Cgroups = c

	// linux-specific configをセット。
	if spec.Linux != nil {
		...

		//デフォルトでpid、network、ipc、uts、mountのnamespaceをロード。
		for _, ns := range spec.Linux.Namespaces {
			t, exists := namespaceMapping[ns.Type]
			if !exists {
				return nil, fmt.Errorf("namespace %q does not exist", ns)
			}
			if config.Namespaces.Contains(t) {
				return nil, fmt.Errorf("malformed spec file: duplicated ns %q", ns)
			}
			config.Namespaces.Add(t, ns.Path)
		}
		if config.Namespaces.Contains(configs.NEWNET) && config.Namespaces.PathOf(configs.NEWNET) == "" {
			config.Networks = []*configs.Network{
				{
					Type: "loopback",
				},
			}
		}
		// user namespaceがある場合は、ユーザのroot IDとgroup IDを設定。
		if config.Namespaces.Contains(configs.NEWUSER) {
			if err := setupUserNamespace(spec, config); err != nil {
				return nil, err
			}
		}
		...
  		// Intelチップパラメータを設定。
		if spec.Linux.IntelRdt != nil {
			config.IntelRdt = &configs.IntelRdt{}
			if spec.Linux.IntelRdt.L3CacheSchema != "" {
				config.IntelRdt.L3CacheSchema = spec.Linux.IntelRdt.L3CacheSchema
			}
			if spec.Linux.IntelRdt.MemBwSchema != "" {
				config.IntelRdt.MemBwSchema = spec.Linux.IntelRdt.MemBwSchema
			}
		}
	}
	if spec.Process != nil {
  		// oomスコアを設定。
		config.OomScoreAdj = spec.Process.OOMScoreAdj
		// privileges
		config.NoNewPrivileges = spec.Process.NoNewPrivileges
 		// umask
		config.Umask = spec.Process.User.Umask
		// selinux
		if spec.Process.SelinuxLabel != "" {
			config.ProcessLabel = spec.Process.SelinuxLabel
		}
		// コンテナに一部の特権を付与。
		if spec.Process.Capabilities != nil {
			config.Capabilities = &configs.Capabilities{
				Bounding:    spec.Process.Capabilities.Bounding,
				Effective:   spec.Process.Capabilities.Effective,
				Permitted:   spec.Process.Capabilities.Permitted,
				Inheritable: spec.Process.Capabilities.Inheritable,
				Ambient:     spec.Process.Capabilities.Ambient,
			}
		}
	}
	// コンテナライフサイクルフック
	/*
	preStart : initプロセスを開始する前のフックコメントによると、フックは放棄されました
	CreateRuntime : pivot_rootの実行前。
	CreateContainer : CreateRuntimeの実行後。
	Poststart :ユーザプロセスの実行前。
	StartContainer :ユーザプロセスが開始されておらずcreatedの状態。
	poststart :initプロセスの初期化を受信後。
	startContainer initプロセスが開始プロセスの情報を受信後。
	Poststop 
	*/
	createHooks(spec, config)
	config.Version = specs.Version
	return config, nil
}
loadFactory

loadFactoryメソッドは、contextを引数としてLinuxFactoryを返します。これは、libcontainer.New()メソッドで実装されています。libcontainer.New()メソッドはLinuxFactoryを返し、InitPathが「/proc/self/exe」(現在のexeファイル、つまりrunc本体)に設定されています。InitArgsは、os.Args[0]が現在のruncのパスであり、基本的にInitPathと同じです。つまり、runc initとなります。

/* utils/utils_linux.go */
func loadFactory(context *cli.Context) (libcontainer.Factory, error) {
        ...
        return libcontainer.New(abs, cgroupManager, intelRdtManager,
                libcontainer.CriuPath(context.GlobalString("criu")),
                libcontainer.NewuidmapPath(newuidmap),
                libcontainer.NewgidmapPath(newgidmap))
}

/* libcontainer/factory_linux.go */
func New(root string, options ...func(*LinuxFactory) error) (Factory, error) {
        ...
	l := &LinuxFactory{
		Root:      root,	
		InitPath:  "/proc/self/exe", /*runc本体*/
		InitArgs:  []string{os.Args[0], "init"},	 /*runc init*/
		Validator: validate.New(),
		CriuPath:  "criu",
	}
        ...
        return l, nil
}
factory.Create

factory.Createメソッドは、LinuxFactoryに記録されたInitPathやInitArgsをlinuxContainerに割り当てて返します。

/*libcontainer/factory_linux.go*/
func (l *LinuxFactory) Create(id string, config *configs.Config) (Container, error) {
        ...
	c := &linuxContainer{
		id:            id,//コンテナID
		root:          containerRoot, /*コンテナ状態ファイルの格納先:デフォルトは/run/runc/{container id}/*/
		config:        config,
		initPath:      l.InitPath, /* /proc/self/exe(runc本体) */
		initArgs:      l.InitArgs,/*runc init*/ 
		criuPath:      l.CriuPath,
		newuidmapPath: l.NewuidmapPath,
		newgidmapPath: l.NewgidmapPath,
		cgroupManager: l.NewCgroupsManager(config.Cgroups, nil),
	}
        ...
        return c, nil
}

ここまでがstartContainerメソッドから呼び出されたcreateContainerがlinuxContainerを生成する過程になります。
次は、startContainerメソッドから呼び出されるrunner.runメソッドを説明します。

runner.run

runnerは、これまでの設定内容をロードして、内部でrunc initプロセスを実行します。
このrunc initプロセスは、namespaceを設定するnsexecやコンテナ(execveで変身)となる重要なプロセスです。

詳細は後述しますが、runc creareプロセスとrunc initプロセス(3つのプロセスに分裂)が連携することで最終的にコンテナが実行(exec)されます。

① :現在実行中(runner.run)のrunc creareプロセス:コンテナ生成フローを最上位で制御
② :①に実行されたrunc init親プロセス(nsexec):runc creareとの中継ぎや子プロセスの同期を行う中間管理職のようなプロセス。
③ :②に実行されたrunc init子プロセス(nsexec):実際にnamespaceを設定
④ :③に実行されたrunc init孫プロセス(nsexec(c言語)→go言語→コンテナ(exec)):②と③が役目を終えてexit()しても生き残り、最終的にコンテナとプロセス。

runner.runメソッドの入力はspec.Process構造体で、内容はconfig.json由来です。
spec.Processはconfig.jsonのprocess項目がGo言語フォーマットに変換されただけの内容のため、注意を払う必要はありません。

/* utils_linux.go */
func startContainer(context *cli.Context, spec *specs.Spec, action CtAct, criuOpts *libcontainer.CriuOpts) (int, error) {
	id := context.Args().First()
...
	container, err := createContainer(context, id, spec)
...
        // runnerは、これまでの設定内容をロードしてrunc initプロセス(nsexecやコンテナ本体)を実行。
	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,
		init:            true,
		logLevel:        logLevel,
	}
	return r.run(spec.Process)
}

runner.runメソッドは、下記の2つの処理で構成されています。
1.newProcess()メソッドを呼び出して、spec.Processを使用してlibcontainer.Processを作成します。2番目の引数がtrueになっていますが、これは、新しく作成されたプロセスが、コンテナの最初のプロセスになることを意味します。
2.r.action(CT_ACT_CREATE)の値に従って、libcontainer.Processを操作する方法を決定します。

/*utils_linux.go*/
func (r *runner) run(config *specs.Process) (int, error) {
        ...
	process, err := newProcess(*config, r.init, r.logLevel)
        ...
	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")
        ...
	return status, err
}
newProcess

libcontainer.Process構造体は/libcontainer/process.goで定義されていて、そのほとんどはspec.Process(config.json:process)由来のものです。

/*libcontainer/process.go*/
package libcontainer
...
type Process struct {
	Args string
	Env string
	User string
	AdditionalGroups string
	Cwd string
	Stdin io.Reader
	Stdout io.Writer
	Stderr io.Writer
	ExtraFiles *os.File
	ConsoleWidth  uint16
	ConsoleHeight uint16
	Capabilities *configs.Capabilities
	AppArmorProfile string
	Label string
	NoNewPrivileges *bool
	Rlimits []configs.Rlimit
	ConsoleSocket *os.File
	Init bool
	ops processOperations
	LogLevel string
}
linuxContainer.Start

linuxContainer.Start(r.container.Start)メソッドは、下記の処理を実行します 。
1.fifoの作成:後で使用するexec.fifoという名前のパイプを作成します
2.start()メソッドを呼び出します(Sが大文字と小文字で違うメソッドです)

/*libcontainer/container_linux.go*/
func (c *linuxContainer) Start(process *Process) error {
        ...
	if process.Init {
		if err := c.createExecFifo(); err != nil { //後でexecを呼び出すためのFIFOの作成
			return err
		}
	}
	if err := c.start(process); err != nil { 
		if process.Init {
			c.deleteExecFifo()
		}
		return err
	}
	return nil
}


Start()メソッドから呼び出されたstart()メソッドは、下記の処理を実行します 。
1.ParentProcessを作成する
2.このParentProcessのstart()メソッドを呼び出します

func (c *linuxContainer) start(process *Process) (retErr error) {
	parent, err := c.newParentProcess(process)
        ...
	err := parent.start();//runc init親プロセスを実行
        ...
	if process.Init {
		if c.config.Hooks != nil {
			s, err := c.currentOCIState()
			if err != nil {
				return err
			}
			// poststart hook を実行
			if err := c.config.Hooks[configs.Poststart].RunHooks(s); err != nil {
				if err := ignoreTerminateErrors(parent.terminate()); err != nil {
					logrus.Warn(errorsf.Wrapf(err, "Running Poststart hook"))
				}
				return err
			}
		}
	}
	...
}

runCでは、parentProcessは次のような抽象インターフェイスです。

/*libcontainer/process_linux.go*/
type 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)
}
newParentProcess

newParentProcess()メソッドにはinitProcessとsetnsProcessの2つの実装があり、前者はコンテナ内に最初のプロセスを作成するために使用され、後者は既存のコンテナ内に新しいプロセスを作成するために使用されます。今回は、p.Init = trueであるため、initProcessが作成されます。

/*libcontainer/container_linux.go*/
func (c *linuxContainer) newParentProcess(p *Process) (parentProcess, error) {
	//runc initの②親プロセスと③子プロセスの間に通信用Socket Pairを作成。
	parentPipe, childPipe, err := utils.NewSockPair("init")
	//exec.Cmdの作成 
	cmd, err := c.commandTemplate(p, childPipe) 

	if !p.Init {
		return c.newSetnsProcess(p, cmd, parentPipe, childPipe) 
	}
	//extraFilesにexec.fifoを追加
	if err := c.includeExecFifo(cmd); err != nil { 
		return nil, newSystemErrorWithCause(err, "including execfifo in cmd.Exec setup")
	}
	//initProcessオブジェクトを生成し、_LIBCONTAINER_INITTYPEをstandardに設定。
	//主に名前空間とoomスコアを格納するbootstrapDataを生成。
	//init-parent、init-childの通信チャネルを設定。
	//initProcessを作成 
	return c.newInitProcess(p, cmd, parentPipe, childPipe)
}

newParentProcess()メソッドには4つのステップがあります。最初の3ステップは、initProcessを生成するステップ4の準備処理です。

1.SocketPairを作成します。作成されたSocketPairはinitProcessに入力されます。

2.exec.Cmdを作成します。コードは次のとおりです。ここでは、cmdによって実行される実行可能プログラムを設定し、パラメータはc.initPath、つまり、LinuxFactoryの「/proc/self/exe」と「init」から取得します。新しく実行されたプログラムはrunC自体ですが、パラメータはinitになり、外部で作成されたSocketPairのchildPipeがcmd.ExtraFilesに配置され、_LIBCONTAINER_INITPIPE =%dがcmd.Envに追加されます。

/*libcontainer/container_linux.go*/
func (c *linuxContainer) commandTemplate(p *Process, childPipe *os.File) (*exec.Cmd, error) {
	cmd := exec.Command(c.initPath, c.initArgs[1:]...)
	cmd.Args[0] = c.initArgs[0]
	//stdストリームはruncinitコマンドに渡され、最終的にrunc initを介してコンテナに渡されます
    	cmd.Stdin = p.Stdin
	cmd.Stdout = p.Stdout
	cmd.Stderr = p.Stderr
	cmd.ExtraFiles = append(cmd.ExtraFiles, p.ExtraFiles...)
	//childPipeは、親プロセス(現在のruncプロセス)との通信に使用されます
	cmd.ExtraFiles = append(cmd.ExtraFiles, childPipe)
	//stdストリームは最初の3つのfd番号(0、1、2)を占めるため、fd番号を環境変数_LIBCONTAINER_INITPIPEを介してruncinitに渡します
	//したがって、fdは3(stdioFdCount)を追加する必要があります
	cmd.Env = append(cmd.Env,
		fmt.Sprintf("_LIBCONTAINER_INITPIPE=%d", stdioFdCount+len(cmd.ExtraFiles)-1),
	)
	...
	return cmd, nil
}

3.includeExecFifo()メソッドは、事前に作成されたfifoを開き、そのfdをcmd.ExtraFilesに配置し、_LIBCONTAINER_FIFOFD =%dをcmd.Envに記録します。

4.newInitProcess()メソッドでInitProcessを作成します。ここでは、最初に_LIBCONTAINER_INITTYPE = "standard"をcmd.Envに追加し、次に新しいコンテナで作成する必要のある名前空間のタイプを構成から読み取り、使用するために変数データに格納します。最後にInitProcessを作成します。事前に作成されたリソースと変数はここで利用されます。

/*libcontainer/container_linux.go*/
func (c *linuxContainer) newInitProcess(p *Process, cmd *exec.Cmd, parentPipe, childPipe *os.File) (*initProcess, error) {
	//initタイプは、環境変数_LIBCONTAINER_INITTYPEを介してstandard(initStandard)に設定されます
	cmd.Env = append(cmd.Env, "_LIBCONTAINER_INITTYPE="+string(initStandard))
	nsMaps := make(map[configs.NamespaceType]string)
	for _, ns := range c.config.Namespaces {
		if ns.Path != "" {
			nsMaps[ns.Type] = ns.Path
		}
	}
	_, sharePidns := nsMaps[configs.NEWPID]
	data, err := c.bootstrapData(c.config.Namespaces.CloneFlags(), nsMaps)
	if err != nil {
		return nil, err
	}
	return &initProcess{
		cmd:       cmd,
		childPipe:     childPipe,
		parentPipe:   parentPipe,
		manager:     c.cgroupManager,
		intelRdtManager:   c.intelRdtManager,
		config:     c.newInitConfig(p),
		container:   c,
		process:   p,          
		bootstrapData:  data,
		sharePidns:   sharePidns,
	}, nil
}

ここまで、linuxContainerのstart()メソッドでparentProcessオブジェクトが作成される過程を確認しました。
次は、parentProcessのstart()メソッドが呼び出されます。

func (c *linuxContainer) start(process *Process) error {
	parent, err := c.newParentProcess(process)  //parentProcessの作成 

	err := parent.start();  //parentProcessを起動
	...
parentProcess.start()

前述したように、newParentProcess()は、config.jsonから得られる設定に従って、initProcessオブジェクトを生成します。
このinitProcessには、以下の情報が含まれています。

cmdは、コンテナが実行する実行ファイルの名前、つまり"/proc/self/exe init(runc init)"を記録しています。
cmd.Envには、名前付きパイプのファイルディスクリプタが記録され、exec.fifoには、_LIBCONTAINER_FIFOFD=%dという名前のSocketPairが作成され、そのchildPipe側のディスクリプタが記録されます。 LIBCONTAINER_INITTYPE="standard"はコンテナ内のプロセスが初期プロセスであることを意味します。
initProcessのbootstrapDataには、新しいコンテナのためにどのNamespaceを作成するかが記録されています。

/* libcontainer/process_linux.go */
func (p *initProcess) start() error {
	p.cmd.Start() //runc initコマンドを実行(プロセス生成)        
	io.Copy(p.parentPipe, p.bootstrapData)//起動されたrunc initプロセスに設定情報を提供
	...
}

runc init

parentProcess.start()は、cmdに設定された実行ファイル"/proc/self/exe init(runc init)"を起動します。
この関数は、コマンドを実行するために新しいプロセスを起動します。(runc init 親プロセス②)
/proc/self/exeはruncプログラムそのものなので、これはrunc initを実行することになります。

io.Copyは、p.bootstrapDataからp.parentPipeを介してrunc init(nsexec)にデータを送信します。
新しいプロセスを作成する理由は、作成したコンテナが別のネームスペースで実行される必要があるためです。
これは、setns()システムコールによって行われますが、setns manページには次のような記載があります。

マルチスレッドのプロセスでは、setns()でuser namespaceを変更することはできません。

Goのランタイムはマルチスレッドであるため、setns()はGoのランタイムが始まる前に設定しなければならず、Goのランタイムが始まる前にcgoが埋め込まれた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

nsenterパッケージは、cgo経由で埋め込まれたC言語のコードで、nsexec()を呼び出します。

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

次に、nsexec()はコンテナのために新しい名前空間を作成します。
余談ですが、CVE-2019-5736の緩和処理も書かれていますね。

/* 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 };

	//環境変数_LIBCONTAINER_INITPIPEから子パイプのfd番号を取得します
	pipenum = initpipe();
	if (pipenum == -1)
		return;

	//CVE-2019-5736の脆弱性を回避するために、現在のバイナリファイルがコピーされています
        //これはコンテナが/proc/self/exeを介してホストバイナリにアクセスできないようにするためです
	if (ensure_cloned_binary() < 0)
		bail("could not ensure we are a cloned binary");

	//initpipeからnamespaceの構成を読み取ります
	nl_parse(pipenum, &config);
   
	...

上記のCコードでは、initpipe()で親プロセスで設定されたパイプ(_LIBCONTAINER_INITPIPEで記録されたファイルディスクリプタ)を読込み、nl_parseを呼び出してこのパイプから変数configに設定を読み込んでいます。通信相手は、親(①runc create)プロセスです。 次の図のように、runc createプロセスは、このパイプを通じて、新しいコンテナの構成をrunc init(nsexec)に送ります。

f:id:FallenPigeon:20210501113928p:plain

送信されたデータは、linuxContainerのbootstrapData()関数でnetlink msg形式のメッセージとしてラップされています。
この時点で、子プロセスは親プロセスから名前空間の構成を取得します。
次に、nsexec()は自分の子や孫との通信のために、さらに2つのソケットペアを作成します。

/*libcontainer/nsenter/nsexec.c*/
void nsexec(void)
{
	...
	/* セットアップが完了したときに子②に知らせることができるようにsocketpairを作成*/
	if (socketpair(AF_LOCAL, SOCK_STREAM, 0, sync_child_pipe) < 0) 
		bail("failed to setup sync pipe between parent and child");

	/*孫③と同期する新しいsocketpairを作成*/
	if (socketpair(AF_LOCAL, SOCK_STREAM, 0, sync_grandchild_pipe) < 0)
		bail("failed to setup sync pipe between parent and grandchild");
   
}

以下のスイッチ文で構成された処理では、現在のプロセス(②runc init)がcloneシステムコールを介して子プロセス(③runc init)を作成し、子プロセスがclone()システムコールを介して孫プロセス(④runc init)を作成します。

このようにrunc initには3つのプロセスがあります。

②runc init親プロセス:最初のプロセスはbootstrapDataを読み取り、2番目のプロセスのユーザマップの設定を完了します 。
③runc init子プロセス:2番目のプロセスは、namespaceのcreate/joinを行います。 
④runc init孫プロセス:3番目のプロセスは、nsexecの処理でcgroup namesapceの設定を完了し、runc initのgo言語処理でコンテナ内の環境を準備し、最後にコンテナのエントリーポイントを実行します。

②runc initも③runc initも最終的にはexit(0)で終了しますが、④runc initは終了せずに、runc initコマンドの後半部分(Go実装)を実行し続けます。そのため、最初の①runc createプロセスと④runc initプロセスだけが残ります。

f:id:FallenPigeon:20210502181030p:plain

enum sync_t {
	SYNC_USERMAP_PLS = 0x40,	/* Request parent to map our users. */
	SYNC_USERMAP_ACK = 0x41,	/* Mapping finished by the parent. */
	SYNC_RECVPID_PLS = 0x42,	/* Tell parent we're sending the PID. */
	SYNC_RECVPID_ACK = 0x43,	/* PID was correctly received by parent. */
	SYNC_GRANDCHILD = 0x44,	/* The grandchild is ready to run. */
	SYNC_CHILD_FINISH = 0x45,	/* The child or grandchild has finished. */
};
...
	switch (current_stage) {

	//②runc init親プロセス
	//新しい子(STAGE_CHILD)プロセスを作成し、そのuid_mapとgid_mapを作成します。
	//子プロセスは孫プロセスを作成し、PIDを送信します。
	case STAGE_PARENT:{
			...
			stage1_pid = clone_parent(&env, STAGE_CHILD);//runc init子プロセスのclone
			while (!stage1_complete) {
				...
				switch (s) {
				case SYNC_USERMAP_PLS: //子プロセスからのユーザマップの設定依頼
					...
					update_uidmap(config.uidmappath, stage1_pid, config.uidmap, config.uidmap_len);
					update_gidmap(config.gidmappath, stage1_pid, config.gidmap, config.gidmap_len);
					...
					s = SYNC_USERMAP_ACK;
					if (write(syncfd, &s, sizeof(s)) != sizeof(s)) {//完了通知
						...
					}
				case SYNC_RECVPID_PLS:
					...
					/* 孫プロセスのPIDを取得*/
					if (read(syncfd, &stage2_pid, sizeof(stage2_pid)) != sizeof(stage2_pid)) {
						sane_kill(stage1_pid, SIGKILL);
						sane_kill(stage2_pid, SIGKILL);
						bail("failed to sync with stage-1: read(stage2_pid)");
					}

					/* Send ACK. */
					s = SYNC_RECVPID_ACK;
					if (write(syncfd, &s, sizeof(s)) != sizeof(s)) {//完了通知
						...
					}
					...
					//子と孫のpidをrunc createに送信
					len =
					    dprintf(pipenum, "{\"stage1_pid\":%d,\"stage2_pid\":%d}\n", stage1_pid,
						    stage2_pid);
					...
					break;
				case SYNC_CHILD_FINISH://子プロセスの処理が完了したことを受信
					write_log(DEBUG, "stage-1 complete");
					stage1_complete = true;
					break;
				}
			}
			...
				write_log(DEBUG, "signalling stage-2 to run");
				s = SYNC_GRANDCHILD;
				if (write(syncfd, &s, sizeof(s)) != sizeof(s)) {//孫プロセスへの同期開始を通知
					...
				}

				if (read(syncfd, &s, sizeof(s)) != sizeof(s))
						...

				switch (s) {
				case SYNC_CHILD_FINISH://孫プロセスの処理が完了したことを受信
					write_log(DEBUG, "stage-2 complete");
					stage2_complete = true;
					break;
				default:
					bail("unexpected sync value: %u", s);
				}
				...
		}
		break;

	//③runc init子プロセス
	// 要求された名前空間の共有を解除します。 
	//特に、CLONE_NEWUSER(user namespace)を要求された場合は、親プロセスにユーザマッピングを設定するように依頼します。 
	//次に、PID名前空間の孫を作成し、PIDを親プロセスに送信します。
	case STAGE_CHILD:{

			//他のnamespaceや特権チェックのコンテキストとして使用されるため、最初にuser namespaceを設定します。 
			if (config.cloneflags & CLONE_NEWUSER) {
				write_log(DEBUG, "unshare user namespace");
				if (unshare(CLONE_NEWUSER) < 0)//user namespaceを作成
						...
				//子プロセスにはユーザマッピングを操作する権限がないため、親プロセスに設定を要求します。
				s = SYNC_USERMAP_PLS;
				if (write(syncfd, &s, sizeof(s)) != sizeof(s))

				/* ... wait for mapping ... */
				write_log(DEBUG, "request stage-0 to map user namespace");
				if (read(syncfd, &s, sizeof(s)) != sizeof(s))//親プロセスの完了通知を受信
					...
				/* setresuidで自身をuser namespace内のrootに昇格 */
				if (setresuid(0, 0, 0) < 0)
					bail("failed to become root in user namespace");
			}

			//user namespace以外のnamespaceをunshare(分離)します。(cgroup,pidを除く)
			if (unshare(config.cloneflags & ~CLONE_NEWCGROUP) < 0)
				...

			//pid namespcaeのために再度フォークします。setns(2)またはunshare(2)は、呼出し元プロセスのpid namespaceを変更しません。
			//変更すると、呼び出し元自身のPIDの概念が変更され、アプリケーションやライブラリがクラッシュします。
			//新しいpid namespaceはforkされたプロセスに割り当てられます。(runc initプロセスが3つある理由の一つ)

			stage2_pid = clone_parent(&env, STAGE_INIT);//孫プロセスの作成

			s = SYNC_RECVPID_PLS;
			//親プロセスに孫プロセスのPIDを受信するよう通知
			if (write(syncfd, &s, sizeof(s)) != sizeof(s)) {
					...
			}
			//孫プロセスのPIDを親プロセスに送信
			if (write(syncfd, &stage2_pid, sizeof(stage2_pid)) != sizeof(stage2_pid)) {
				...
			}

			//親プロセスの完了通知を受信
			if (read(syncfd, &s, sizeof(s)) != sizeof(s)) {
				...
			}
			if (s != SYNC_RECVPID_ACK) {
				...
			}

			//子プロセスの処理が完了したことを親プロセスに通知
			s = SYNC_CHILD_FINISH;
			if (write(syncfd, &s, sizeof(s)) != sizeof(s)) {
				...
			}
		}
		break;

	//④runc init孫プロセス
	//最上位の親がp.manager.Apply()でcgroupのセットアップを完了するまで待ちます。
	case STAGE_INIT:{

			//cgroup namespace
			if (config.cloneflags & CLONE_NEWCGROUP) {
				uint8_t value;
				if (read(pipenum, &value, sizeof(value)) != sizeof(value))
					bail("read synchronisation value failed");
				if (value == CREATECGROUPNS) {
					write_log(DEBUG, "unshare cgroup namespace");
					if (unshare(CLONE_NEWCGROUP) < 0)
						bail("failed to unshare cgroup namespace");
				} else
					bail("received unknown synchronisation value");
			}

			//孫プロセスの処理が完了したことを親プロセスに通知
			s = SYNC_CHILD_FINISH;
			if (write(syncfd, &s, sizeof(s)) != sizeof(s))
				bail("failed to sync with patent: write(SYNC_CHILD_FINISH)");

			//孫プロセスはnsexecの処理が完了した後もexitしないため、runc init (go実装)の処理が継続される。
			write_log(DEBUG, "<= nsexec container setup");
			write_log(DEBUG, "booting up go runtime ...");
			return;
		}
		break;
	default:
		bail("unknown stage '%d' for jump value", current_stage);
	}


ここでrunc createプロセスに戻ります。

func (p *initProcess) start() (retErr error) {
	defer p.messageSockPair.parent.Close()
	// runc initを実行(nsexecが実行される)
	err := p.cmd.Start()
	...
	// 子プロセスのcgroupを制限
	if err := p.manager.Apply(p.pid()); err != nil {
		return newSystemErrorWithCause(err, "applying cgroup configuration for process")
	}
	if p.intelRdtManager != nil {
		if err := p.intelRdtManager.Apply(p.pid()); err != nil {
			return newSystemErrorWithCause(err, "applying Intel RDT configuration for process")
		}
	}
	//bootstrapDataをrunc initプロセスに送信し、runcのinitプロセスがそれを受け取り、自分の名前空間を設定するなどの作業を行う。
	if _, err := io.Copy(p.messageSockPair.parent, p.bootstrapData); err != nil {
		return newSystemErrorWithCause(err, "copying bootstrap data to pipe")
	}

	//initpipe経由で子プロセスのpidを取得する
	childPid, err := p.getChildPid()

	//子プロセスのファイルディスクリプタのパスを取得する
	fds, err := getPipeFds(childPid)

	// 新しいcgroupの名前空間(コンテナ用)を設定するようにinitプロセスに通知する
	if p.config.Config.Namespaces.Contains(configs.NEWCGROUP) && p.config.Config.Namespaces.PathOf(configs.NEWCGROUP) == "" {
		if _, err := p.messageSockPair.parent.Write([]byte{createCgroupns}); err != nil {
			return newSystemErrorWithCause(err, "sending synchronization value to init process")
		}
	}

	// nsexecプロセスの実行を待機する
	// pidの情報はinitpipeで取得する
	// nsexecではbootstrapDataを受け取り、プロセスの名前空間を設定する
  // その後、goで書かれたrunc initの処理に移る。
	if err := p.waitForChildExit(childPid); err != nil {
		return newSystemErrorWithCause(err, "waiting for our first child to exit")
	}
	
	...
	// init構成をinitプロセスに送信
 	if err := p.sendConfig(); err != nil {
		return newSystemErrorWithCause(err, "sending config to init process")
	}
	var (
		sentRun    bool
		sentResume bool
	)

	// init processと進行状況の同期
	// parseSyncはソケットが閉じられるまでループします。
	ierr := parseSync(p.messageSockPair.parent, func(sync *syncT) error {
		switch sync.Type {
		// initプロセスの準備完了時
		case procReady:
			// rlimitsの設定
			if err := setupRlimits(p.config.Rlimits, p.pid()); err != nil {
				return newSystemErrorWithCause(err, "setting rlimits for ready process")
			}
			// フックが実行できるのはマウント・ネームスペースがない場合のみ
                        //通常はマウント・ネームスペースが必要
			if !p.config.Config.Namespaces.Contains(configs.NEWNS) {
				// Setup cgroup before the hook, so that the prestart and CreateRuntime hook could apply cgroup permissions.
				if err := p.manager.Set(p.config.Config); err != nil {
					return newSystemErrorWithCause(err, "setting cgroup config for ready process")
				}
				...
				if p.config.Config.Hooks != nil {
					s, err := p.container.currentOCIState()
					if err != nil {
						return err
					}
					// 子プロセスのpid設定
					s.Pid = p.cmd.Process.Pid
 					// Statusの作成
					s.Status = specs.StateCreating
					hooks := p.config.Config.Hooks
					
					if err := hooks[configs.Prestart].RunHooks(s); err != nil {
						return err
					}
					if err := hooks[configs.CreateRuntime].RunHooks(s); err != nil {
						return err
					}
				}
			}

			// generate a timestamp indicating when the container was started
			p.container.created = time.Now().UTC()
			p.container.state = &createdState{
				c: p.container,
			}

			state, uerr := p.container.updateState(p)
			if uerr != nil {
				return newSystemErrorWithCause(err, "store init state")
			}
			p.container.initProcessStartTime = state.InitProcessStartTime

			// 子プロセスが、動作を続けている状態
			if err := writeSync(p.messageSockPair.parent, procRun); err != nil {
				return newSystemErrorWithCause(err, "writing syncT 'run'")
			}
			sentRun = true

		// initプロセスからフックシグナルを受け取り、pivot_rootが実行される直前の状態。
		case procHooks:
			// プロセスのcgroupの設定
			if err := p.manager.Set(p.config.Config); err != nil {
				return newSystemErrorWithCause(err, "setting cgroup config for procHooks process")
			}

			if p.intelRdtManager != nil {
				if err := p.intelRdtManager.Set(p.config.Config); err != nil {
					return newSystemErrorWithCause(err, "setting Intel RDT config for procHooks process")
				}
			}
			// hookの実行
			if p.config.Config.Hooks != nil {
				s, err := p.container.currentOCIState()
				if err != nil {
					return err
				}
				s.Pid = p.cmd.Process.Pid
				s.Status = specs.StateCreating
				hooks := p.config.Config.Hooks

				if err := hooks[configs.Prestart].RunHooks(s); err != nil {
					return err
				}
				if err := hooks[configs.CreateRuntime].RunHooks(s); err != nil {
					return err
				}
			}
			// pivot_rootの実行を再開・継続するようにinitプロセスに通知する
			if err := writeSync(p.messageSockPair.parent, procResume); err != nil {
				return newSystemErrorWithCause(err, "writing syncT 'resume'")
			}
			sentResume = true
		}

		return nil
	})
	// initコールバックを待ち, コールバックが成功した場合は、
        //残りの設定、つまりライフサイクルのHOOK呼び出しを完了します
	if !sentRun {
		return newSystemErrorWithCause(ierr, "container init")
	}
	// フックのコールバックが成功するのを待つ
	if p.config.Config.Namespaces.Contains(configs.NEWNS) && !sentResume {
		return newSystemError(errors.New("could not synchronise after executing prestart and CreateRuntime hooks with container process"))
	}
	// init pipeを閉じる
	if err := unix.Shutdown(int(p.messageSockPair.parent.Fd()), unix.SHUT_WR); err != nil {
		return newSystemErrorWithCause(err, "shutting down init pipe")
	}

	if ierr != nil {
		p.wait()
		return ierr
	}
	return nil
}

runc init(After nsexec)

この時点で、runc init 1号とrunc init 2号はexit()で終了しています。
runc init 3はnsexecで名前空間が設定された後、残りのコンテナ設定をgo言語で行い、最後にコンテナのエントリポイントを実行(exec)します。
runc init 3は最初にlibcontainer.Newを介してLinuxFactoryを作成し、LinuxFactoryのStartInitialization()メソッドを呼び出します。

...
var initCommand = cli.Command{
	Name:  "init",
	Usage: `initialize the namespaces and launch the process (do not call it outside of runc)`,
	Action: func(context *cli.Context) error {
		factory, _ := libcontainer.New("")
		if err := factory.StartInitialization(); err != nil {
			os.Exit(1)
		}
		panic("libcontainer: container init failed to exec")
	},
}
func (l *LinuxFactory) StartInitialization() (err error) {
        // initpipeのファイルディスクリプタを取得します
	envInitPipe := os.Getenv("_LIBCONTAINER_INITPIPE")
	pipefd, err := strconv.Atoi(envInitPipe)
	if err != nil {
		return fmt.Errorf("unable to convert _LIBCONTAINER_INITPIPE=%s to int: %s", envInitPipe, err)
	}
	// 親プロセスと通信するためのパイプを用意します
	pipe := os.NewFile(uintptr(pipefd), "pipe")
	defer pipe.Close()

	// runc createは、type standerと exec.fifo パイプラインを初期化します。
	fifofd := -1
	envInitType := os.Getenv("_LIBCONTAINER_INITTYPE")
	it := initType(envInitType)
	if it == initStandard {
		envFifoFd := os.Getenv("_LIBCONTAINER_FIFOFD")
		if fifofd, err = strconv.Atoi(envFifoFd); err != nil {
			return fmt.Errorf("unable to convert _LIBCONTAINER_FIFOFD=%s to int: %s", envFifoFd, err)
		}
	}

	// 継承されたプロセス環境をクリアする
	os.Clearenv()
	...
	//今回はstarndar linuxStandardInitを返します。execを実行している場合はlinuxSetnsInitを返します。
	i, err := newContainerInit(it, pipe, consoleSocket, fifofd)
	if err != nil {
		return err
	}
    //  Initの処理に入る。
	return i.Init()
}
linuxStandardInit.Init
/*libcontainer/standard_init_linux.go*/
func (l *linuxStandardInit) Init() error {
	// ネットワークを設定する
	if err := setupNetwork(l.config); err != nil {
		return err
	}
	// ルーティングを設定する
	if err := setupRoute(l.config.Config); err != nil {
		return err
	}

	// selinux設定
	selinux.GetEnabled()
	//ルートディレクトリのマウント、外部ボリュームのマウント、デバイスの作成を行って、ルートファイルシステムを整理します。
	// runc createにprestart hook(pivot_rootまたはchange_rootの前)を通知し、ルートディレクトリを隔離します。
	// prestart hookは、pivot_rootやchange_rootが実行される前に処理されます。
	if err := prepareRootfs(l.pipe, l.config); err != nil {
		return err
	}
	...

	// 最終的なルートファイルシステムを完成させる。主に必要なマウントポイントをマウントしていく。
	if l.config.Config.Namespaces.Contains(configs.NEWNS) {
		if err := finalizeRootfs(l.config.Config); err != nil {
			return err
		}
	}

	// ホスト名を設定する
	if hostname := l.config.Config.Hostname; hostname != "" {
		if err := unix.Sethostname(byte(hostname)); err != nil {
			return errors.Wrap(err, "sethostname")
		}
	}
	// apparmerの設定
	if err := apparmor.ApplyProfile(l.config.AppArmorProfile); err != nil {
		return errors.Wrap(err, "apply apparmor profile")
	}

	//net.ipv4.ip_forwardを/proc/sys/net/ipv4/ip_forwardに変換するなど、システムプロパティを書込む
	for key, value := range l.config.Config.Sysctl {
		if err := writeSystemProperty(key, value); err != nil {
			return errors.Wrapf(err, "write sysctl key %s", key)
		}
	}
	...
	if err != nil {
		return errors.Wrap(err, "get pdeath signal")
	}
	// 特権(昇格)の設定
	if l.config.NoNewPrivileges {
		if err := unix.Prctl(unix.PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); err != nil {
			return errors.Wrap(err, "set nonewprivileges")
		}
	}

	// 基本的な初期化が完了し、execを実行する準備ができたことをrunc createに通知
	if err := syncParentReady(l.pipe); err != nil {
		return errors.Wrap(err, "sync ready")
	}
	...
	...
	// seccompの設定
	if l.config.Config.Seccomp != nil && !l.config.NoNewPrivileges {
		if err := seccomp.InitSeccomp(l.config.Config.Seccomp); err != nil {
			return err
		}
	}
	// コンテナ用の特権ケイパビリティ、ユーザ、および作業ディレクトリを構成
	if err := finalizeNamespace(l.config); err != nil {
		return err
	}
	...
	// コンテナのコンテキスト、ルートファイルシステムなどはすべて準備できているため、実行可能ファイルがコンテナに存在するかどうかを確認
	// 現在のルートファイルシステムでは、実行可能なruncファイルが見つかるはずです
	name, err := exec.LookPath(l.config.Args[0])
	if err != nil {
		return err
	}
	// initfileのクローズ
	l.pipe.Close()
	// コンテナ開始コマンドを実行する前に、runc startでexec.fifoパイプがオープンされるのを待機します。
	// /proc/self/fd/にfd -> /run/runc//があります。
	fd, 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")
	}
	//exec.fifoパイプラインにデータを書き込むと、init processがブロックされ、runc start呼出しを待機します。
	if _, err := unix.Write(fd, byte("0")); err != nil {
		return newSystemErrorWithCause(err, "write 0 exec fifo")
	}
	//exec.fifoをクローズ
	unix.Close(l.fifoFd)

	s := l.config.SpecState
	s.Pid = unix.Getpid()

	// 状態をcreatedに設定
	s.Status = specs.StateCreated
	if err := l.config.Config.Hooks[configs.StartContainer].RunHooks(s); err != nil {
		return err
	}
	// コンテナの開始
	// execでコンテナ処理に移行
	if err := unix.Exec(name, l.config.Args[0:], os.Environ()); err != nil {
		return newSystemErrorWithCause(err, "exec user process")
	}
	return nil
}

runc start

/*start.go*/
...
                #コンテナの参照
		container, err := getContainer(context)
		if err != nil {
			return err
		}
		...
		switch status {
		case libcontainer.Created:
			...
			// initの代わりにexecコンテナプロセスを実行します
			if err := container.Exec(); err != nil {
				return err
			}
			if notifySocket != nil {
				return notifySocket.waitForContainer(container)
			}
			return nil
		case libcontainer.Stopped:
			return errors.New("cannot start a container that has stopped")
		case libcontainer.Running:
			return errors.New("cannot start an already running container")
		default:
			return fmt.Errorf(
...
func (c *linuxContainer) exec() error {
	path := filepath.Join(c.root, execFifoFilename)
	pid := c.initProcess.pid()
	// /run/runc//exec.fifoを読み取ります。
	blockingFifoOpenCh := awaitFifoOpen(path)
	// exec.fifoファイルの内容を取得するか、プロセスがゾンビプロセスになるのを待機します。
	for {
		select {
		case result := <-blockingFifoOpenCh:
			// handleFifoResultは、最終的にコンテンツを読み取った後、exec.fifoを削除します。
			return handleFifoResult(result)

		case <-time.After(time.Millisecond * 100):
			stat, err := system.Stat(pid)
			if err != nil || stat.State == system.Zombie {
				if err := handleFifoResult(fifoOpen(path, false)); err != nil {
					return errors.New("container process is already dead")
				}
				return nil
			}
		}
	}
}

*1:constructor