概述
RunC 是 Docker 貢獻出來魁淳,按照 OCI 運行時標準制定的一種具體實現(xiàn),是一個可執(zhí)行應用程序包工具荧降」诮荆可通過OCI 鏡像格式標準文件包 bundles 來創(chuàng)建和運行容器以及對容器的生命周期管理詳情參考。 本文《附錄一》附有使用 runc 工具命令來運行容器的示例可供參考绸贡。
從 docker 容器的整個架構和執(zhí)行流程視角來看是由 containerd-shim 組件調(diào)用了 runc 來創(chuàng)建和運行容器盯蝴,其創(chuàng)建容器時的配置文件/run/docker/libcontainerd/?$containerID/config.json,進行讀取轉(zhuǎn)化為 spec 標準作為 runc 創(chuàng)建容器全局配置參數(shù)。
本文將重點聚焦在 runc run 命令的執(zhí)行代碼的整個流程上(容器的創(chuàng)建至容器的運行)的解析听怕,而有些細節(jié)實現(xiàn)比如 namespace 捧挺、cgroup 、網(wǎng)絡等將在此套系列文檔有詳細介紹可供參閱尿瞭。
從 runc run 代碼執(zhí)行結構可簡單分為為四塊執(zhí)行組成部分:1. Run 命令執(zhí)行入口 2. 容器對象創(chuàng)建 3. 容器執(zhí)行 init 初始化 4. 容器用戶程序與運行 ,本文下面將順序地進行展開解析闽烙。
CLI run 執(zhí)行入口
Cli app 的 run 命令執(zhí)行,讀取命令參數(shù)和讀取與轉(zhuǎn)化 config.json 為 spec 標準配置声搁。
!FILENAME run.go:65
Action: func(context *cli.Context) error {
// 命令參數(shù)校驗
if err := checkArgs(context, 1, exactArgs); err != nil {
return err
}
// 獲取"pid-file"傳參配置黑竞,轉(zhuǎn)化為絕對路徑
if err := revisePidFile(context); err != nil {
return err
}
// 讀取 config.json
spec, err := setupSpec(context)
if err != nil {
return err
}
// +startContainer() 啟動容器
status, err := startContainer(context, spec, CT_ACT_RUN, nil)
if err == nil {
// 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 err
},
StartContainer() 啟動容器頂層代碼執(zhí)行過程:
- 讀取傳入的 container id 參數(shù)
- 通過 spec 配置與 id 等傳參創(chuàng)建容器對象
- 構建 runner 啟動器并執(zhí)行
!FILENAME utils_linux.go:430
func startContainer(context *cli.Context, spec *specs.Spec, action CtAct, criuOpts *libcontainer.CriuOpts) (int, error) {
// 通過 spec 創(chuàng)建容器結構捕发,在 createContainer 中將 spec 轉(zhuǎn)換為了 runc 的 container config*
id := context.Args().First() //命令行輸入的container id參數(shù)
if id == "" {
return -1, errEmptyID
}
notifySocket := newNotifySocket(context, os.Getenv("NOTIFY_SOCKET"), id)
if notifySocket != nil {
notifySocket.setupSpec(context, spec)
}
// +創(chuàng)建容器對象
container, err := createContainer(context, id, spec)
if err != nil {
return -1, err
}
if notifySocket != nil {
err := notifySocket.setupSocket()
if err != nil {
return -1, err
}
}
// Support on-demand socket activation by passing file descriptors into the container init process.
listenFDs := []*os.File{}
if os.Getenv("LISTEN_FDS") != "" {
listenFDs = activation.Files(false)
}
logLevel := "info"
if context.GlobalBool("debug") {
logLevel = "debug"
}
// 構建 runner 啟動器
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, // CT_ACT_RUN 執(zhí)行標志
criuOpts: criuOpts, // criu 熱遷移選項
init: true, // 用于設置 process.Init 字段
logLevel: logLevel, // 日志級別 default info
}
return r.run(spec.Process) // run() 啟動
}
Container 容器對象創(chuàng)建
RunC 代碼實例化容器對象的代碼模式是通過工廠方法實現(xiàn),實例化 LinuxFactory 類型工廠和 linuxContainer 類型容器對象很魂。
!FILENAME utils_linux.go:230
func createContainer(context *cli.Context, id string, spec *specs.Spec) (libcontainer.Container, error) {
rootlessCg, err := shouldUseRootlessCgroupManager(context)
if err != nil {
return nil, err
}
// spec 轉(zhuǎn)換 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
}
factory, err := loadFactory(context) //+ 創(chuàng)建工廠實例
if err != nil {
return nil, err
}
return factory.Create(id, config) //+ 工廠實例化容器對象
}
loadFactory() 創(chuàng)建容器工廠 libcontainer.Factory 扎酷,配置 cgroup 管理器 、root path 遏匆、intel RDT 管理器 霞玄、user map、熱遷移路徑拉岁。
!FILENAME utils_linux.go:31
// loadFactory returns the configured factory instance for execing containers.
func loadFactory(context *cli.Context) (libcontainer.Factory, error) {
root := context.GlobalString("root")
abs, err := filepath.Abs(root) // 根目錄絕對路徑
if err != nil {
return nil, err
}
cgroupManager := libcontainer.Cgroupfs // cgroup Manger 默認為 Cgroupfs
rootlessCg, err := shouldUseRootlessCgroupManager(context)
if err != nil {
return nil, err
}
if rootlessCg {
cgroupManager = libcontainer.RootlessCgroupfs
}
if context.GlobalBool("systemd-cgroup") { // systemd-cgroup 是否全局指定開啟
if systemd.UseSystemd() {
cgroupManager = libcontainer.SystemdCgroups
} else {
return nil, fmt.Errorf("systemd cgroup flag passed, but systemd support for managing cgroups is not available")
}
}
intelRdtManager := libcontainer.IntelRdtFs // intel RDT
if !intelrdt.IsCatEnabled() && !intelrdt.IsMbaEnabled() {
intelRdtManager = nil
}
newuidmap, err := exec.LookPath("newuidmap") // newuidmap 容器內(nèi)外 uid 映射
if err != nil {
newuidmap = ""
}
newgidmap, err := exec.LookPath("newgidmap") // newgidmap 容器內(nèi)外 uid 映射
if err != nil {
newgidmap = ""utils_linux.go
}
// 創(chuàng)建容器工廠
return libcontainer.New(abs, cgroupManager, intelRdtManager,
libcontainer.CriuPath(context.GlobalString("criu")),
libcontainer.NewuidmapPath(newuidmap),
libcontainer.NewgidmapPath(newgidmap))
}
創(chuàng)建LinuxFactory類型的factoy對象,用于容器對象的創(chuàng)建工廠
!FILENAME libcontainer/factory_linux.go:131
func New(root string, options ...func(*LinuxFactory) error) (Factory, error) {
if root != "" {
//確保存儲容器狀態(tài)的根目錄創(chuàng)建
if err := os.MkdirAll(root, 0700); err != nil {
return nil, newGenericError(err, SystemError)
}
}
l := &LinuxFactory{
// 存儲容器狀態(tài)的根目錄惰爬,默認"/run/runc/"
Root: root,
// 指向當前的 exe 程序喊暖,即 runc 本身
InitPath: "/proc/self/exe",
// os.Args[0] 是當前 runc 的路徑,本質(zhì)上和 InitPath 是一樣的撕瞧,即 runc init
InitArgs: []string{os.Args[0], "init"},
// 配置校驗器對象
Validator: validate.New(),
// 熱遷移路徑設置
CriuPath: "criu",
}
Cgroupfs(l) //為 LinuxFactory 配置 NewCgroupsManage實現(xiàn) func
for _, opt := range options {
if opt == nil {
continue
}
if err := opt(l); err != nil {
return nil, err
}
}
return l, nil
}
基于全局配置陵叽,容器工廠創(chuàng)建 linuxContainer 容器對象
!FILENAME libcontainer/factory_linux.go:188
func (l *LinuxFactory) Create(id string, config *configs.Config) (Container, error) {
// 確保containerRoot目錄被創(chuàng)建
if l.Root == "" {
return nil, newGenericError(fmt.Errorf("invalid root"), ConfigInvalid)
}
// 校驗參數(shù)
if err := l.validateID(id); err != nil {
return nil, err
}
if err := l.Validator.Validate(config); err != nil {
return nil, newGenericError(err, ConfigInvalid)
}
// 容器根路徑
containerRoot, err := securejoin.SecureJoin(l.Root, id)
if err != nil {
return nil, err
}
if _, err := os.Stat(containerRoot); err == nil {
return nil, newGenericError(fmt.Errorf("container with id exists: %v", id), IdInUse)
} else if !os.IsNotExist(err) {
return nil, newGenericError(err, SystemError)
}
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)
}
// 創(chuàng)建 linux 容器結構
c := &linuxContainer{
id: id, // 容器 ID
root: containerRoot, // 容器狀態(tài)文件存放目錄,默認是 /run/runc/$容器ID/
config: config, // 容器配置
initPath: l.InitPath, // /proc/self/exe丛版,就是runc
initArgs: l.InitArgs, // 即runc init
criuPath: l.CriuPath, // 熱遷移path "criu"
// Uid / Gid 配置
newuidmapPath: l.NewuidmapPath,
newgidmapPath: l.NewgidmapPath,
// cgroup配置
cgroupManager: l.NewCgroupsManager(config.Cgroups, nil),
}
// 英特爾RDT(資源調(diào)配技術)配置
if intelrdt.IsCatEnabled() || intelrdt.IsMbaEnabled() {
c.intelRdtManager = l.NewIntelRdtManager(config, id, "")
}
c.state = &stoppedState{c: c} // 開始置為"stopped"狀態(tài)
return c, nil
}
Runner 執(zhí)行和 init 容器初始化
根據(jù) startContainer() 頂層的執(zhí)行流程巩掺,在創(chuàng)建容器化對象后,構建 runner 對象并執(zhí)行run()页畦。此 run() 則是容器的運行階段的入口胖替,它包含兩大塊執(zhí)行過程:Start() 容器環(huán)境初始化啟動 和 exec () 容器用戶程序執(zhí)行,start() 初始化啟動過程比較復雜一些容器的初始工作都在此過程中豫缨,包含cgroup 独令、namespace等等核心初始化工作(專文說明),而 start() 過程則比較簡單僅取消bootstrap進程的阻塞態(tài)好芭,讓其完成執(zhí)行entrypoint燃箭。后面將進行詳細的描述。
Runner.run() 創(chuàng)建初始化進程對象舍败,調(diào)用 linuxContainer.run() 運行進程
utils_linux.go:271
func (r *runner) run(config *specs.Process) (int, error) {
var err error
defer func() {
if err != nil {
r.destroy()
}
}()
if err = r.checkTerminal(config); err != nil {
return -1, err
}
// +基于config 創(chuàng)建 init process對象 (指定 "run init")
process, err := newProcess(*config, r.init, r.logLevel)
if err != nil {
return -1, err
}
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++ {
_, err = os.Stat(fmt.Sprintf("/proc/self/fd/%d", i))
if err != nil {
return -1, errors.Wrapf(err, "please check that preserved-fd %d (of %d) is present", i-baseFd, r.preserveFDs)
}
process.ExtraFiles = append(process.ExtraFiles, os.NewFile(uintptr(i), "PreserveFD:"+strconv.Itoa(i)))
}
rootuid, err := r.container.Config().HostRootUID()
if err != nil {
return -1, err
}
rootgid, err := r.container.Config().HostRootGID()
if err != nil {
return -1, err
}
var (
detach = r.detach || (r.action == CT_ACT_CREATE)
)
// 處理io和tty相關配置
handler := newSignalHandler(r.enableSubreaper, r.notifySocket)
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)
case CT_ACT_RUN:
err = r.container.Run(process) // +調(diào)用linuxContainer.run()
default:
panic("Unknown action")
}
if err != nil {
return -1, err
}
if err = tty.waitConsole(); err != nil {
r.terminate(process)
return -1, err
}
if err = tty.ClosePostStart(); err != nil {
r.terminate(process)
return -1, err
}
if r.pidFile != "" {
if err = createPidFile(r.pidFile, process); err != nil {
r.terminate(process)
return -1, err
}
}
status, err := handler.forward(process, tty, detach)
if err != nil {
r.terminate(process)
}
if detach {
return 0, nil
}
r.destroy()
return status, err
}
newProcess() 基于 spec 配置初始化并創(chuàng)建libconatiner.process 進程對象返回
!FILENAME utils_linux.go:106
func newProcess(p specs.Process, init bool, logLevel string) (*libcontainer.Process, error) {
lp := &libcontainer.Process{
Args: p.Args,
Env: p.Env,
User: fmt.Sprintf("%d:%d", p.User.UID, p.User.GID), // uid:gid
Cwd: p.Cwd,
Label: p.SelinuxLabel, // selinux 標簽
NoNewPrivileges: &p.NoNewPrivileges,
AppArmorProfile: p.ApparmorProfile, // Apparmor 配置
Init: init, // runc init
LogLevel: logLevel,
}
if p.ConsoleSize != nil { // console 窗口設置
lp.ConsoleWidth = uint16(p.ConsoleSize.Width)
lp.ConsoleHeight = uint16(p.ConsoleSize.Height)
}
if p.Capabilities != nil { // capabilities 配置
lp.Capabilities = &configs.Capabilities{}
lp.Capabilities.Bounding = p.Capabilities.Bounding
lp.Capabilities.Effective = p.Capabilities.Effective
lp.Capabilities.Inheritable = p.Capabilities.Inheritable
lp.Capabilities.Permitted = p.Capabilities.Permitted
lp.Capabilities.Ambient = p.Capabilities.Ambient
}
for _, gid := range p.User.AdditionalGids { // gid 配置
lp.AdditionalGroups = append(lp.AdditionalGroups, strconv.FormatUint(uint64(gid), 10))
}
for _, rlimit := range p.Rlimits { // limit 資源限制配置
rl, err := createLibContainerRlimit(rlimit)
if err != nil {
return nil, err
}
lp.Rlimits = append(lp.Rlimits, rl)
}
return lp, nil
}
LinuxContainer.Run() 為上層 CT_ACT_RUN 執(zhí)行流程調(diào)用:
- Start() Init 進程執(zhí)行啟動
- exec() 用戶進程EntryPoint 執(zhí)行
libcontainer/container_linux.go:250
func (c *linuxContainer) Run(process *Process) error {
if err := c.Start(process); err != nil { // +容器環(huán)境 init 啟動
return err
}
if process.Init {
return c.exec() // +EntryPoint 執(zhí)行
}
return nil
}
調(diào)用 linuxContainer.start()
libcontainer/container_linux.go:233
func (c *linuxContainer) Start(process *Process) error {
//...
if err := c.start(process); err != nil { // +linuxContainer.start() 運行 process
if process.Init {
c.deleteExecFifo()
}
return err
}
return nil
}
linuxContainer.start() 為一個完整的上層容器實始化執(zhí)行流程代碼招狸,首先通過上面?zhèn)鲄⒌?process 進程對象創(chuàng)建 “父” 進程并啟動(核心邏輯處),完成啟動后保存容器狀態(tài)到 state.json 文件(默認"/run/runc/$containerID/ state.json")邻薯,最后如果容器有定義運行后勾子將被調(diào)用執(zhí)行裙戏。
libcontainer/container_linux.go:335
func (c *linuxContainer) start(process *Process) error {
// +創(chuàng)建的父進程
parent, err := c.newParentProcess(process)
if err != nil {
return newSystemErrorWithCause(err, "creating new parent process")
}
parent.forwardChildLogs()
// +啟動父進程
if err := parent.start(); err != nil {
// terminate the process to ensure that it properly is reaped.
if err := ignoreTerminateErrors(parent.terminate()); err != nil {
logrus.Warn(err)
}
return newSystemErrorWithCause(err, "starting container process")
}
// 容器啟動狀態(tài) state 保存(寫入 state.json 文件)
c.created = time.Now().UTC()
if process.Init {
c.state = &createdState{
c: c,
}
state, err := c.updateState(parent)
if err != nil {
return err
}
c.initProcessStartTime = state.InitProcessStartTime
if c.config.Hooks != nil {
s, err := c.currentOCIState()
if err != nil {
return err
}
// postStrat 容器運行后勾子執(zhí)行
for i, hook := range c.config.Hooks.Poststart {
if err := hook.Run(s); err != nil {
if err := ignoreTerminateErrors(parent.terminate()); err != nil {
logrus.Warn(err)
}
return newSystemErrorWithCausef(err, "running poststart hook %d", i)
}
}
}
}
return nil
}
newParentProcess() 創(chuàng)建父進程的過程:
- 創(chuàng)建父子進程通信的 pipe ( bootstrapData 配置數(shù)據(jù)用此傳遞)
- 創(chuàng)建 cmd 對象 ( 此處的cmd 對象就是執(zhí)行 runc init ,后面有詳述 )
- 返回 newInitProcess() initProcess 對象
libcontainer/container_linux.go:441
func (c *linuxContainer) newParentProcess(p *Process) (parentProcess, error) {
// 創(chuàng)建用于父子進程通信的 pipe
parentInitPipe, childInitPipe, err := utils.NewSockPair("init")
if err != nil {
return nil, newSystemErrorWithCause(err, "creating new init pipe")
}
messageSockPair := filePair{parentInitPipe, childInitPipe}
//...
// +創(chuàng)建父進程的 cmd
cmd, err := c.commandTemplate(p, childInitPipe, childLogPipe)
if err != nil {
return nil, newSystemErrorWithCause(err, "creating new command template")
}
//...
// +返回標準 init 進程
return c.newInitProcess(p, cmd, messageSockPair, logFilePair)
}
創(chuàng)建父進程的 cmd 對象
libcontainer/container_linux.go:473
func (c *linuxContainer) commandTemplate(p *Process, childInitPipe *os.File, childLogPipe *os.File) (*exec.Cmd, error) {
// 這里可以看到 cmd 就是 runc init
cmd := exec.Command(c.initPath, c.initArgs[1:]...)
cmd.Args[0] = c.initArgs[0]
// 將設置給容器 entrypoint 的 std 流給了 runc init 命令,這些流最終會通過 runc init 傳遞給 entrypoint
cmd.Stdin = p.Stdin
cmd.Stdout = p.Stdout
cmd.Stderr = p.Stderr
cmd.Dir = c.config.Rootfs
if cmd.SysProcAttr == nil {
cmd.SysProcAttr = &syscall.SysProcAttr{}
}
cmd.Env = append(cmd.Env, fmt.Sprintf("GOMAXPROCS=%s", os.Getenv("GOMAXPROCS")))
cmd.ExtraFiles = append(cmd.ExtraFiles, p.ExtraFiles...)
if p.ConsoleSocket != nil {
cmd.ExtraFiles = append(cmd.ExtraFiles, p.ConsoleSocket)
cmd.Env = append(cmd.Env,
fmt.Sprintf("_LIBCONTAINER_CONSOLE=%d", stdioFdCount+len(cmd.ExtraFiles)-1),
)
}
// 這個 childInitPipe 用于跟父進程通信(父進程就是當前這個 runc 進程)
cmd.ExtraFiles = append(cmd.ExtraFiles, childInitPipe)
// 通過環(huán)境變量 _LIBCONTAINER_INITPIPE 把 fd 號傳遞給 runc init弛说,由于 std 流會占用前三個 fd 編號(0挽懦,1,2)
// 所以 fd 要加上 3(stdioFdCount)
cmd.Env = append(cmd.Env,
fmt.Sprintf("_LIBCONTAINER_INITPIPE=%d", stdioFdCount+len(cmd.ExtraFiles)-1),
fmt.Sprintf("_LIBCONTAINER_STATEDIR=%s", c.root),
)
//...
return cmd, nil
}
返回 initProcess 對象
libcontainer/container_linux.go:512
func (c *linuxContainer) newInitProcess(p *Process, cmd *exec.Cmd, messageSockPair, logFilePair filePair) (*initProcess, error) {
// 這里通過環(huán)境變量 _LIBCONTAINER_INITTYPE 設置 init 類型為 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]
// 構造 namespace 配置木人,然后序列化成字節(jié)數(shù)據(jù)
data, err := c.bootstrapData(c.config.Namespaces.CloneFlags(), nsMaps)
if err != nil {
return nil, err
}
init := &initProcess{
cmd: cmd, // cmd 對象,也就是 run int
messageSockPair: messageSockPair, // 通信 sockpair
logFilePair: logFilePair,
manager: c.cgroupManager,
intelRdtManager: c.intelRdtManager,
config: c.newInitConfig(p),
container: c, // 容器對象
process: p, // 傳參的進程對象
bootstrapData: data, // namespaces 配置序列化數(shù)據(jù)
sharePidns: sharePidns,
}
c.initProcess = init
return init, nil //返回 init 進程對象
}
InitProcess.start() 則是容器運行最核心的代碼執(zhí)行邏輯塊:
當前執(zhí)行進程我們稱之為“bootstrap進程“信柿,cmd.Start() 實則執(zhí)行了 "runc init" 命令冀偶,同時也激活了nsenter 模塊C 代碼的優(yōu)先執(zhí)行配置namespace (詳細可參閱《RunC 源碼通讀指南之 NameSpace》),完后返回執(zhí)行 init Go 代碼部分完成后續(xù)的初始化工作渔嚷,最后向管道 exec.fifo 進行寫操作进鸠,init 進程進入阻塞狀態(tài)等待信號完成容器內(nèi)的entrypoint執(zhí)行。
!FILENAME libcontainer/process_linux.go:282
func (p *initProcess) start() error {
defer p.messageSockPair.parent.Close()
// 當前執(zhí)行空間進程稱為bootstrap進程
// 啟動了 cmd形病,即啟動了 runc init 命令,創(chuàng)建 runc init 子進程
// 同時也激活了C代碼nsenter模塊的執(zhí)行(為了 namespace 的設置 clone 了三個進程parent客年、child、init)
// C 代碼執(zhí)行后返回 go 代碼部分,最后的 init 子進程為了好區(qū)分此處命名為" nsInit "(即配置了Namespace的init)
// runc init go代碼為容器初始化其它部分(網(wǎng)絡漠吻、rootfs量瓜、路由、主機名途乃、console绍傲、安全等)
err := p.cmd.Start()
//...
// 為進程 runc init 應用 Cgroup (p.cmd.Process.Pid())
if err := p.manager.Apply(p.pid()); err != nil {
return newSystemErrorWithCause(err, "applying cgroup configuration for process")
}
//...
// messageSockPair 管道寫入 bootstrapData
if _, err := io.Copy(p.messageSockPair.parent, p.bootstrapData); err != nil {
return newSystemErrorWithCause(err, "copying bootstrap data to pipe")
}
// 獲取 nsInit pid
childPid, err := p.getChildPid()
if err != nil {
return newSystemErrorWithCause(err, "getting the final child's pid from pipe")
}
//...
// 為 nsInit 進程應用 Cgroup
if err := p.manager.Apply(childPid); err != nil {
return newSystemErrorWithCause(err, "applying cgroup configuration for process")
}
// 為 child 進程應用 intel RDT
if p.intelRdtManager != nil {
if err := p.intelRdtManager.Apply(childPid); err != nil {
return newSystemErrorWithCause(err, "applying Intel RDT configuration for process")
}
}
// 設置 cgroup namesapce
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")
}
}
// 等待子進程退出
if err := p.waitForChildExit(childPid); err != nil {
return newSystemErrorWithCause(err, "waiting for our first child to exit")
}
//...
// 創(chuàng)建網(wǎng)絡接口
if err := p.createNetworkInterfaces(); err != nil {
return newSystemErrorWithCause(err, "creating network interfaces")
}
// 發(fā)送 initConfig 進程配置到 messageSockPair.parent 管道
if err := p.sendConfig(); err != nil {
return newSystemErrorWithCause(err, "sending config to init process")
}
var (
sentRun bool
sentResume bool
)
// 解析runc init子進程的所有同步消息,當io.EOF返回
ierr := parseSync(p.messageSockPair.parent, func(sync *syncT) error {
switch sync.Type {
case procReady: //
// 配置 limit 資源限制
if err := setupRlimits(p.config.Rlimits, p.pid()); err != nil {
return newSystemErrorWithCause(err, "setting rlimits for ready process")
}
// // prestart hook 啟動前執(zhí)行勾子
if !p.config.Config.Namespaces.Contains(configs.NEWNS) {
// Setup cgroup before prestart hook, so that the prestart 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.intelRdtManager != nil {
if err := p.intelRdtManager.Set(p.config.Config); err != nil {
return newSystemErrorWithCause(err, "setting Intel RDT config for ready process")
}
}
if p.config.Config.Hooks != nil {
s, err := p.container.currentOCIState()
if err != nil {
return err
}
// initProcessStartTime hasn't been set yet.
s.Pid = p.cmd.Process.Pid
s.Status = "creating"
for i, hook := range p.config.Config.Hooks.Prestart {
if err := hook.Run(s); err != nil {
return newSystemErrorWithCausef(err, "running prestart hook %d", i)
}
}
}
}
// 與子進程 runC init 同步
if err := writeSync(p.messageSockPair.parent, procRun); err != nil {
return newSystemErrorWithCause(err, "writing syncT 'run'")
}
sentRun = true
case procHooks: // prochook 勾子執(zhí)行
// 配置 cgroup
if err := p.manager.Set(p.config.Config); err != nil {
return newSystemErrorWithCause(err, "setting cgroup config for procHooks process")
}
// 配置 intel RDT 資源管理
if p.intelRdtManager != nil {
if err := p.intelRdtManager.Set(p.config.Config); err != nil {
return newSystemErrorWithCause(err, "setting Intel RDT config for procHooks process")
}
}
if p.config.Config.Hooks != nil {
//...
// 執(zhí)行勾子定義任務
for i, hook := range p.config.Config.Hooks.Prestart {
if err := hook.Run(s); err != nil {
return newSystemErrorWithCausef(err, "running prestart hook %d", i)
}
}
}
// 與子進程 runc-init 同步
if err := writeSync(p.messageSockPair.parent, procResume); err != nil {
return newSystemErrorWithCause(err, "writing syncT 'resume'")
}
sentResume = true
default:
return newSystemError(fmt.Errorf("invalid JSON payload from child"))
}
return nil
})
//...
return nil
}
RunC Init 容器初始化
Nsenter 模塊C 代碼執(zhí)行邏輯
RunC init 命令執(zhí)行 Go 調(diào)用 C 代碼稱之 preamble ,即在 import nsenter 模塊時機將會在 Go 的 runtime 啟動之前耍共,先執(zhí)行此先導代碼塊烫饼,nsenter 的初始化 init(void) 方法內(nèi)對 nsexec() 調(diào)用
!FILENAME init.go:10
_ "github.com/opencontainers/runc/libcontainer/nsenter"
!FILENAME libcontainer/nsenter/nsenter.go:3
package nsenter
/*
#cgo CFLAGS: -Wall
extern void nsexec();
void __attribute__((constructor)) init(void) {
nsexec();
}
*/
import "C"
nsexec clone 三個進程:
- 第一個進程稱為“ parent ”,讀取 bootstrapData 并解析為 Config试读,對 User map 設置杠纵,并通過消息協(xié)調(diào)后面兩個進程的運行管理,在收到 grandchild 回復任務完成消息后退出钩骇。
- 第二個進程稱為“ child ”,由 Parent 創(chuàng)建比藻,完成 namespace 的設置 ,fork 出 grandChild 進程并發(fā)送給Parent 后發(fā)送任務完成消息后退出倘屹。
- 第三個進程稱為“ grandChild ”或" init "韩容,進行最后的環(huán)境準備工作(sid、uid唐瀑、gid群凶、cgroup namespace),執(zhí)行完成后return 至 init Go runtime 代碼處繼續(xù)執(zhí)行最后進入 go 代碼哄辣。
!FILENAME libcontainer/nsenter/nsexec.c:575
void nsexec(void)
{
//...
switch (setjmp(env)) {
//...
case JUMP_PARENT:{
//..
}
case JUMP_CHILD:{
//...
}
case JUMP_INIT:{
//...
}
//...
}
注:此塊詳細代碼解析專文請參閱《RunC 源碼通讀指南之 NameSpace》
RunC init (Go 代碼部分)執(zhí)行邏輯
創(chuàng)建 factory 對象请梢,執(zhí)行 factory.StartInitialization() => linuxStandardInit.Init() 完成容器的相關初始化配置(網(wǎng)絡/路由、rootfs力穗、selinux毅弧、console、主機名当窗、apparmor够坐、Sysctl、seccomp、capability 等)
!FILENAME init.go:15
func init() {
//...
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("") // +創(chuàng)建 factory 對象
if err := factory.StartInitialization(); err != nil { // +執(zhí)行 init 初始化
os.Exit(1)
}
panic("libcontainer: container init failed to exec")
},
}
libcontainer.New() 創(chuàng)建 factory 對象返回
!FILENAME libcontainer/factory_linux.go:131
func New(root string, options ...func(*LinuxFactory) error) (Factory, error) {
//...
l := &LinuxFactory{
//...
}
//...
return l, nil
}
factory.StartInitialization() 初始化
!FILENAME libcontainer/factory_linux.go:282
func (l *LinuxFactory) StartInitialization() (err error) {
//...
i, err := newContainerInit(it, pipe, consoleSocket, fifofd)
//...
// newContainerInit()返回的initer實現(xiàn)對象的Init()方法調(diào)用 "linuxStandardInit.Init()"
return i.Init()
}
創(chuàng)建 container 容器對象
!FILENAME libcontainer/factory_linux.go:188
func (l *LinuxFactory) Create(id string, config *configs.Config) (Container, error) {
// 創(chuàng)建 linux 容器結構
c := &linuxContainer{
//...
}
return c, nil
}
linuxContainer.Init() 對網(wǎng)絡/路由元咙、rootfs梯影、selinux、console庶香、主機名甲棍、apparmor、sysctl赶掖、seccomp感猛、capability 等容器的相關初始化配置。管道 exec.fifo 進行寫操作奢赂,進入阻塞狀態(tài)等待 runC start
!FILENAME libcontainer/standard_init_linux.go:46
func (l *linuxStandardInit) Init() error {
//...
// 此兩個關于網(wǎng)絡 nework/route 配置陪白,將由網(wǎng)絡專文詳細介紹
// 配置network,
if err := setupNetwork(l.config); err != nil {
return err
}
// 配置路由
if err := setupRoute(l.config.Config); err != nil {
return err
}
// selinux 配置
label.Init()
// 準備 rootfs
if err := prepareRootfs(l.pipe, l.config); err != nil {
return err
}
// 配置 console
if l.config.CreateConsole {
if err := setupConsole(l.consoleSocket, l.config, true); err != nil {
return err
}
if err := system.Setctty(); err != nil {
return errors.Wrap(err, "setctty")
}
}
// 完成 rootfs 設置
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")
}
}
// 應用 apparmor 配置
if err := apparmor.ApplyProfile(l.config.AppArmorProfile); err != nil {
return errors.Wrap(err, "apply apparmor profile")
}
// Sysctl 系統(tǒng)參數(shù)調(diào)節(jié)
for key, value := range l.config.Config.Sysctl {
if err := writeSystemProperty(key, value); err != nil {
return errors.Wrapf(err, "write sysctl key %s", key)
}
}
// path 只讀屬性設置
for _, path := range l.config.Config.ReadonlyPaths {
if err := readonlyPath(path); err != nil {
return errors.Wrapf(err, "readonly path %s", path)
}
}
for _, path := range l.config.Config.MaskPaths {
if err := maskPath(path, l.config.Config.MountLabel); err != nil {
return errors.Wrapf(err, "mask path %s", path)
}
}
// 獲取父進程退出信號
pdeath, err := system.GetParentDeathSignal()
if err != nil {
return errors.Wrap(err, "get pdeath signal")
}
// 設置安全屬性 nonewprivileges
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")
}
}
// 告訴runC進程,我們已經(jīng)完成了初始化工作
if err := syncParentReady(l.pipe); err != nil {
return errors.Wrap(err, "sync ready")
}
// 進程標簽設置
if err := label.SetProcessLabel(l.config.ProcessLabel); err != nil {
return errors.Wrap(err, "set process label")
}
defer label.SetProcessLabel("")
// seccomp配置
if l.config.Config.Seccomp != nil && !l.config.NoNewPrivileges {
if err := seccomp.InitSeccomp(l.config.Config.Seccomp); err != nil {
return err
}
}
// 設置正確的capability膳灶,用戶以及工作目錄
if err := finalizeNamespace(l.config); err != nil {
return err
}
if err := pdeath.Restore(); err != nil {
return errors.Wrap(err, "restore pdeath signal")
}
if unix.Getppid() != l.parentPid {
return unix.Kill(unix.Getpid(), unix.SIGKILL)
}
// 確定用戶指定的容器進程在容器文件系統(tǒng)中的路徑
name, err := exec.LookPath(l.config.Args[0])
if err != nil {
return err
}
// 關閉管道拷泽,告訴runC進程,我們已經(jīng)完成了初始化工作
l.pipe.Close()
// 在exec用戶進程之前等待exec.fifo管道在另一端被打開
// 我們通過/proc/self/fd/$fd打開它
fd, err := unix.Open(fmt.Sprintf("/proc/self/fd/%d", l.fifoFd), unix.O_WRONLY|unix.O_CLOEXEC, 0)
if err != nil {
return newSystemErrorWithCause(err, "open exec fifo")
}
//
// 此處操作應注意袖瞻,作為容器運行的分界線,后面有說明
//
// 向exec.fifo管道寫數(shù)據(jù)拆吆,阻塞聋迎,直到用戶調(diào)用`runc start`,讀取管道中的數(shù)據(jù)
if _, err := unix.Write(fd, []byte("0")); err != nil {
return newSystemErrorWithCause(err, "write 0 exec fifo")
}
// 關閉fifofd管道
unix.Close(l.fifoFd)
// 初始化Seccomp配置
if l.config.Config.Seccomp != nil && l.config.NoNewPrivileges {
if err := seccomp.InitSeccomp(l.config.Config.Seccomp); err != nil {
return newSystemErrorWithCause(err, "init seccomp")
}
}
// 調(diào)用系統(tǒng)exec()命令枣耀,執(zhí)行entrypoint
if err := syscall.Exec(name, l.config.Args[0:], os.Environ()); err != nil {
return newSystemErrorWithCause(err, "exec user process")
}
return nil
}
容器用戶程序與運行
我們可以看到下面容器的運行是非常簡單的實現(xiàn)霉晕,因為在容器的 init 階段已將所有環(huán)境都準備好了,此時只需讀取管道中的數(shù)據(jù)(等同時于 bootstrap 進程發(fā)送繼續(xù)執(zhí)行信號)捞奕,將進程處于阻塞狀態(tài)的 init 進程繼續(xù)后面代碼執(zhí)行用戶定義的entrypoint程序牺堰。
libcontainer/container_linux.go:266
func (c *linuxContainer) exec() error {
path := filepath.Join(c.root, execFifoFilename)
fifoOpen := make(chan struct{})
select {
case <-awaitProcessExit(c.initProcess.pid(), fifoOpen):
return errors.New("container process is already dead")
case result := <-awaitFifoOpen(path):
close(fifoOpen)
if result.err != nil {
return result.err
}
f := result.file
defer f.Close()
if err := readFromExecFifo(f); err != nil { // 讀操作來解除bootstrap阻塞
return err
}
return os.Remove(path)
}
}
最后重新來看看 init 激活后會執(zhí)行的代碼:
!FILENAME libcontainer/standard_init_linux.go:192
func (l *linuxStandardInit) Init() error {
//...
// unix.Write()阻塞
// 初始化Seccomp配置
if l.config.Config.Seccomp != nil && l.config.NoNewPrivileges {
if err := seccomp.InitSeccomp(l.config.Config.Seccomp); err != nil {
return newSystemErrorWithCause(err, "init seccomp")
}
}
// 調(diào)用系統(tǒng)exec()命令蜒灰,執(zhí)行entrypoint
if err := syscall.Exec(name, l.config.Args[0:], os.Environ()); err != nil {
return newSystemErrorWithCause(err, "exec user process")
}
return nil
}
附錄:
附錄一:RunC 創(chuàng)建容器及命令
RunC run 創(chuàng)建與運行容器實例:
# 1. 準備rootfs文件
$> mkdir /mycontainer; cd /mycontainer
$> docker export $(docker create busybox) | tar -C rootfs -xvf -
# 2. 創(chuàng)建一個config.json文件(標準的OCI格式的文件)
$> runc spec
# 3. rootfs和config.json (OCI runtime bundles)都有了就可以創(chuàng)建容器
$> runc run $mycontainerid
篇幅原因不附實例的config.json文件若皱,可參考官方 config.json 和 OCI Runtime spec 運行時規(guī)范(中文)
RunC 容器的整個生命周期管理操作:
# 創(chuàng)建
$> runc create $mycontainerid
# 啟動
$> runc start $mycontainerid
# 查看
$> runc list
# 刪除
$> runc delete $mycontainerid
相關文檔:
《RunC 源碼通讀指南之 Namespace》
《RunC 源碼通讀指南之 Cgroup》
《RunC 源碼通讀指南之 Create & Start》
《RunC 源碼通讀指南之 Networks》
~~ 本文 END ~~