总结:某些代码首先调用创建容器,然后填充结构。 该函数首先处理配置文件、配置、调用remount、调用配置等,进程调用告诉父进程容器已经准备好运行了。 从父进程的角度来看,处理和求和已经完成,它判断和计算总和是否相等。 终于完成了。
简介
主要从命名空间、cgroup、协作文件、运行时(runC)和网络方面了解docker。 接下来,我将花时间分别介绍一下他们。
Docker系列--命名空间解读
docker系列--cgroups解读
Docker系列--] unionfs解读
docker系列--runC解读
docker系列--网络模式解读
namesapce主要用于隔离,cgroups主要用于资源限制,联合文件主要用于镜像的分层存储和管理。 runC 是一个遵循 oci 接口的运行时,通常基于 libcontainer。 组网主要是docker独立组网和多主机通信方式。
runC
RunC 是一个轻量级工具。 用于运行容器。 它只是用来做好一件事。 你可以把它想象成一个命令行小工具,可以让你直接运行容器,而不需要通过 Docker Engine。 事实上,runC是标准化的产物,按照OCI标准创建和运行容器。 开放容器倡议 (OCI) 组织旨在开发容器格式和运行时的开放行业标准。
OCI 由 docker、coreos 和其他容器公司于 2015 年创立。 目前主要有两个标准文档:容器运行时标准(RuntimeSpecification)和容器镜像标准(ImageSpecification)。
runC是用golang语言实现的,基于libcon泰纳图书馆。 从docker1.11开始,docker架构图:
编译
runc目前支持各种架构的Linux平台。 您必须使用 Go 1.6 或更高版本进行构建,某些功能才能正常工作。
要启用 seccomp 支持,必须在平台上安装 libseccomp。
示例:CentOS 为 libseccomp-devel,Ubuntu 为 libseccomp-dev
否则,构建带有 seccomp 支持的 runc 如果您不想,可以添加。运行 make = "" 时构建标签。
# 在 GOPATH/srccd 中创建“github.com/opencontainers” github.com/opencontainersgit clone https://github.com/opencontainers/runccd runcmakesudo make install
编译选项
runc 支持可选的构建标志,用于编译对各种功能的支持。 要添加构建标签并创建选项,您必须设置 BUILDTAGS 变量。
make BUILDTAGS="seccomp apparmor"
构建标签 | 功能 | 依赖项 |
---|---|---|
seccomp | 系统调用过滤 | libseccomp |
selinux | 标记 selinux 进程和挂载 | |
服装 | 服装配置文件支持 | |
环境 | 环境功能支持 | 内核 4.3 |
使用 runC 创建 OCI 捆绑包
要使用 runc,必须使用 OCI 包格式容器。 如果安装了 Docker,则可以使用其导出方法从现有 Docker 容器中检索根文件系统。
# 创建顶级bundle目录 mkdir /mycontainercd /mycontainer# 创建rootfs目录 mkdir rootfs# 通过Docker导出Busybox到rootfs目录 docker export $(docker createvybox) |# tar -C rootfs - xvf -runc 提供了一个spec 命令来生成可编辑的基本模板规范。
runc spec运行容器
首先,准备一个工作目录。 下面所有的操作都会在这个目录下进行,比如mycontainer。
# mkdir mycontainer接下来,为容器镜像准备文件系统。 选择从 docker 镜像中提取。
# mkdir rootfs# docker export $(docker createvybox) | tar -C rootfs-xvf -# ls rootfs bin dev etc home proc root sys tmp usr var拿到rootfs后,我们还根据OCI标准创建了一个配置文件config.json来解释它是如何实现的是必要的。 运行容器。 runc提供了可以自动生成的命令,包括要执行的命令、权限、环境变量等。
# runc spec# lsconfig.json rootfs这将检索 OCI 运行时包的内容。 这个包非常简单,只包含上面列出的两个内容:config.json 文件和 rootfs 文件系统。 config.json的内容比较长,这里就不贴出来了。 不要修改这个,直接使用默认生成的这个文件。 此信息告诉 runc 如何运行容器。 首先我们看一下简单的方法runc run(该命令需要root权限)。 该命令与 docker run 类似。 创建并启动容器:
runc run simplebusybox/ # lsbin dev etc home proc root sys tmp usr var/ # hostnamerunc/ # whoamiroot/ # pwd// # ip addr1: lo: mtu 65536 qdisc noqueue qlen 1000 链接/环回 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 范围主机 lo valid_lft 永远首选_lftforeverinet6::1/128scopehostvalid_lftforeverpreferred_lftforever/ # ps auxPID USER TIME COMMAND 1 root 0:00 sh 11 root 0:00 ps aux此时,另一个请打开一个终端。 可以查看正在运行的容器信息:
runc listID PID STATUS BUNDLE CREATED OWNERsimplebusybox 18073 running /home/cizixs/Workspace/runc/mycontainer 2017-11-02T06:54:52.023379345Z root解释runC代码
总的来说,runC代码比较简单。 它主要引用了 github.com/urfave/cli 库并实现了一组命令。
app.Commands = []cli.Command{checkpointCommand、createCommand、deleteCommand、eventsCommand、execCommand、initCommand、killCommand、list 命令、pause 命令、ps 命令、如果你熟悉restoreCommand、resumeCommand、runCommand、specCommand、startCommand、stateCommand、updateCommand、}docker命令,你应该对此很熟悉。
这些命令的底层是调用libcontainer库来实现具体的操作。
示例:创建命令:var createCommand = cli.Command{ Name: "create",Usage: "创建容器", ArgsUsage: `Where "”是您要启动的容器实例的名称。 您为容器实例指定的名称在主机上必须是唯一的。 `, 描述:`create 命令创建捆绑包容器的实例。 捆绑包是一个目录,其中包含名为specConfig 的规范文件和根文件系统。 spec 文件包含一个 args 参数。 args参数用于指定容器启动时执行的 cify 命令。 要更改启动时运行的命令,请编辑规范中的 args 参数。 有关更多信息,请参阅 runc 规范 --帮助。 `, Flags: []cli.Flag{ cli.StringFlag{ 名称: "bundle, b", 值: "", 用法: `bundle 目录根目录的路径,默认为当前目录`, }, cli. StringFlag{ 名称:“console-socket”,值:“”,用法:“接收引用控制台伪终端主端的文件描述符的 AF_UNIX 套接字的路径”, }, cli.StringFlag{ 名称: "pid-file", value: "",用法: "指定要写入进程ID的文件", },cli.BoolFlag{ 名称:“no-pivot”,用法:“不要使用枢纽根来监禁 rootfs 中的进程。只要 rootfs 位于 ramdisk 之上,就会使用此选项。”, }, cli.BoolFlag{名称:“no-new-keyring”,用法:“不要为容器创建新的会话密钥环。继承会话密钥”,},cli.IntFlag{ 名称:“prepare-fds”,用法:“将 N 个附加文件描述符传递到容器(stdio + $LISTEN_FDS + N 总计)”, }, }, 操作:func(context *cli.Context) error { if err := checkArgs(context, 1, exactArgs) ; err != nil { return err } if err := RevisePidFile(context); 错误 != 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 } // 以 / 结束/ 外部主管将收到退出通知以及正确的退出状态。 os.Exit(status) return nil },}为每个命令参数指定所需的命令行。
具体执行逻辑
其实如果需要更深入的理解,就需要了解libcontainer。
需要理解的主要文件是:factory.go
container.go
process.go
init_linux.go
在这里,我将解释如何创建它。 一个容器来分析和理解上述文件。
首先调用spec, err := setupSpec(context)加载配置文件config.json的内容。 这与前面提到的 OCI 捆绑包有关。
规范,错误 := setupSpec(context) if error != nil {return err }最终生成了一个Spec对象,其spec定义如下。
// Spec是container.type的基本配置。 Spec struct { // 捆绑包遵循的开放容器运行时规范的版本。 Version string `json:"ociVersion"` // 该进程构成容器进程。 Process *Process `json:"process,omitempty"` // Root 配置容器的根文件系统。 Root *Root `json:"root,omitempty"` // hostname 构成容器的主机名。 Hostname string `json:"hostname,omitempty"` // Mount 配置额外的挂载(在 root 之上)。 Mounts []Mount `json: "mounts,omitempty"` // 该钩子配置容器生命周期事件的回调。 Hook *Hook `json:"hooks,omitempty" platform:"linux,solaris"` // Anno包含容器的任意元数据。 Annotations map[string]string `json:"annotations,omitempty"` // Linux 是基于 Linux 的容器的特定于平台 - 的配置。 Linux *Linux `json:"linux,omitempty" platform :"linux"` // Solaris 是基于 Solaris 的容器的特定于平台 - 的配置。 Solaris *Solaris `json:"solaris,omitempty" platform:"solaris"` // Windows 是基于 Windows 的容器的特定于平台 - 的配置。 Windows *Windows `json:"windows,omitempty" platform:"windows"`}接下来,通过调用 status, err := startcontainer(context, spec, CT_ACT_CREATE, nil) Masu. CT_ACT_CREATE 表示创建操作。 CT_ACT_CREATE 是一个枚举。
类型 CtAct uint8const (CT_ACT_CREATE CtAct = iota + 1 CT_ACT_RUN CT_ACT_RESTORE)status, err := startContainer(context, spec, CT_ACT_CREATE, nil)startcontainer具体代码:
func startContainer(context *cli.Context, spec *specs.Spec , Action CtAct, criuOpts *libcontainer.CriuOpts) (int, error) { id := context.Args().First() if id == "" { return -1, errEmptyID }NoticeSocket := newNotifySocket(context, os . Getenv("NOTIFY_SOCKET"), id) if NotifySocket != nil {notifySocket.setupSpec(context, spec) } 容器, err := createContainer(context, id, spec) if err != nil { return -1 , err } ifNoticeSocket != nil { err :=NoticeSocket.setupSocket() if err != nil { return -1, err } } // 通过将文件描述符传递给容器来激活 on-- 需求套接字。支持激活初始化过程。 ListenFDs := []*os.File{} if os.Getenv("LISTEN_FDS") != "" { ListenFDs = Activity.Files(false) } r := &runner{ enableSubreaper: !context.Bool("no [k4 ]subreaper"),shouldDestroy: true,容器:容器,listenFDs:listenFDs,notifySocket:NoticeSocket,consoleSocket:context.String("console-socket"),分离:context.Bool("detach"),pidFile : context .String(“pid-file”),preserveFDs:context.Int(“preserve-fds”),action:action,criuOpts:criuOpts,init:true,}返回r.run(spec.Process)}首先,通过调用container, err := createContainer(context, id, spec)创建容器,然后设置runner结构体r。
func createContainer(context *cli.Context, id string, spec *specs.Spec) (libcontainer.Container, error) { rootless, err := isRootless(context) if err != nil { return nil, err } 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: 规格,Rootless: rootless, }) if err != nil { return nil, err } Factory, err :=loadFactory(context) if err != nil { return nil, err } return Factory.Create(id, config)}注意工厂,err :=loadFactory(context) 和factory.Create(id, config)。 这两个就是上面描述的工厂。 克啊。 工厂根据配置配置创建特定的容器。
最后调用run方法。 run 方法传递一个进程对象,该对象表示有关容器内进程的信息。 也就是上面process.go文件的内容。
// 进程包含在container.type中启动特定应用程序的信息。 Process struct { // Terminal 为容器创建一个交互式终端。 Terminal bool `json:"terminal,omitempty"` // ConsoleSize 指定控制台的大小。 ConsoleSize *Box `json:"consoleSize,omitempty"` // User 指定进程的用户信息。 User User `json:"user"` // Args 指定要运行的应用程序的二进制文件和参数。 Args []string `json:"args"` // Env 设置进程的进程环境。 env []string `json:"env,omitempty"` // cwd 是进程的当前工作目录, // 必须是相对目录访问容器的根目录。 Cwd string `json:"cwd"` // Capability 是为进程保留的 Linux 功能。 Capability *LinuxCapability `json:"capabilities,omitempty" platform:"linux"` // Rlimits 指定应用于进程的 rlimit 选项。 Rlimits []POSIXRlimit `json:"rlimits,omitempty" platform:"linux,solaris"` // NoNewPrivileges 控制容器内的进程是否可以获得额外权限。 NoNewPrivileges bool `json: "noNewPrivileges,omitempty" platform:"linux"` // ApparmorProfile 是容器的 ApparmorProfile string `json:"apparmorProfile,omitempty" platform:"linux"` // 指定容器的 oom_score_adj。 `json:"oomScoreAdj,omitempty" 平台:"linux"` // SelinuxLabel指定容器进程运行的selinux上下文。SelinuxLabel string `json:"selinuxLabel,omitempty" platform:"linux"`}run方法主要是newProcess方法
process, err := newProcess(*config, r.init)newProcess主要处理参数、环境变量、用户权限、工作目录、cpability、资源限制等
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) ) 默认:panic("未知操作") }启动容器代码container.Start(process):
func (c *linuxContainer) start(process *Process) error {parent , err := c.newParentProcess(process) if err != nil {return newSystemErrorWithCause(err, "Creating a new Parent process") } if err :=parent.start(); err != nil { // 终止进程以确保它被正确获取。 if err :=ignoreTerminateErrors(parent.terminate ()); err != nil { logrus.Warn(err) } return newSystemErrorWithCause(err, "starting container process") } // 返回一个时间戳,指示容器何时启动 生成 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 hook != nil { 包,注释 := utils.Annotations(c.c.onfig.Labels) s := configs.HookState{ 版本:c.config.Version,ID:c.id,Pid:parent.pid(),bundle:bundle,注释:注释, } for i,hook:=范围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
1 . 创建parentPipe和childPipe管道对作为runc启动进程与容器中init进程的通信管道
2. 创建命令模板作为启动父进程的模板
3.newInitProcess 封装了 initProcess。 主要任务是为初始化类型添加环境变量,并使用bootstrap数据将namespace、uid/gid映射等信息封装到io.Reader中。newInitProcess
为初始化类型添加环境变量和命名空间。 、uid/gid 映射等信息 uid/gid 映射等信息使用 bootstrapData 函数封装在 io.Reader 中。 Netlink 用于内核间通信并返回 initProcess 结构。
最后调用func (l *linuxStandardInit) Init()错误方法。 这是上面显示的 init_linux.go 文件。
func (l *linuxStandardInit) Init() error { if !l.config.Config.NoNewKeyring {ringname, keepperms, newperms := l.getSessionRingParams() // 不继承父级的会话 keyring 。 , err :=keys.JoinSessionKeyring(ringname) if err != nil { returnerrors.Wrap(err, "join session keyring") } // 使会话密钥环可搜索 if err :=keys.ModKeyringPerm(sessKeyId, keepperms, newperms)错误!= nil { returnerrors.Wrap(err, "mod keyring Permissions") } } if err := setupNetwork(l.config); err != nil { return err } if err := setupRoute(l.config.Config); = nil { return err } label.Init() if err := prepareRootfs(l.pipe, l.config); err != nil { return err } // 配置控制台。 这应该在 rootfs 完成之前完成, // 但要在给用户机会配置他们需要的所有安装之后。 if l.config.CreateConsole { if err := setupConsole(l.consoleSocket, l.config, true); err != nil { return err } if err := system.Setctty(); err != nil { 返回名称。 Wrap(err, "setctty") } } // 对于 .config.C,退出 rootfs 设置。onfig.Namespaces.Contains(configs.NEWNS) { if err := FinalizeRootfs(l.config.Config); err != nil { return err } } if 主机名 := l.config.Config.Hostname != "" { if err := unix.Sethostname([]byte(hostname)); err != nil { returnerrors.Wrap(err, "sethostname") } } if err := apparmor.ApplyProfile(l.config.AppArmorProfile) ; != nil { returnerrors.Wrap(err, "apparmor profile") } if err := label.SetProcessLabel(l.config.ProcessLabel); err != nil { returnerrors.Wrap(err, "设置进程标签") } for key, value := range l.config.Config.Sysctl { if err := writeSystemProperty(key, value); err != nil { returnerrors.Wrapf(err, "write sysctl key %s", key ) } } for _, path := range l.config.Config.ReadonlyPaths { if err := readonlyPath(path); err != nil { returnerrors.Wrapf(err, "只读路径 %s", path) } } for _, path : = range l.config.Config.MaskPaths { if err := MaskPath(path, l.config.Config.MountLabel); err != nil { returnerrors.Wrapf(err, "掩码路径 %s", path) } } pdeath, err := system.GetParentDeathSignal() if err != nil { returnerrors.Wrap(err, "获取 pdeath 信号") } if l.config.NoNewPrivileges { if err := unix.Prctl(unix.PR_SET_NO_NEW_PRIVS, 1 , 0, 0, 0); err != nil { returnerrors.Wrap(err, "set nonewprivileges") } } // 告诉父进程 Execv 已准备就绪。 // 这必须在应用 Seccomp 规则之前完成。// 因为我们需要能够读取和写入套接字。 On error:=syncParentReady(l.pipe); err != nil { returnerrors.Wrap(err, "sync Ready") } // 如果没有 NoNewPrivileges,seccomp 是一个特权操作,因此 // 删除该功能 您需要这样做前。 否则, // 在 execve 之前尽可能晚地运行,以便在执行后进行尽可能少的系统调用。 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 } // FinalizeNamespace 清除父死亡信号 // 用户/组可以更改,因此我们在这里恢复它。 如果错误:= pdeath.Restore();err != nil { returnerrors.Wrap(err, "restore pdeath signal") } // 自 init 进程首次启动以来比较父进程 // 以确保它没有改变。 改变父母意味着父母死了//我们被另一个父母重新抚养长大,所以我们应该自杀//以免给别人带来问题。 if unix.Getppid() != l.parentPid { return unix.Kill(unix.Getpid(), unix .SIGKILL) } // 在等待参数存在之前检查参数, // 返回创建时的错误。 name, err := exec.LookPath(l.config.Args[0]) if err ! = nil { return err } // 关闭管道以表明初始化已完成。 l.pipe.Close() // 在 exec[ 之前等待另一侧的 FIFO 打开。k4]ing // 用户进程。 // 给定的 fd 是 fifo 本身的 O_PATH fd,因此通过 /proc/self/fd/$fd 打开它。 在Linux上,可以通过///proc重新打开O_PATH 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 ) , "打开 exec fifo") } if _, err := unix.Write(fd, []byte("0")); err != nil { return newSystemErrorWithCause(err, "write 0 exec fifo") } / / 在执行前关闭 O_PATH fifofd fd,因为内核以错误的顺序重置转储文件。 这已在较新的内核中得到修复,但我将其留在这里是为了防止 // CVE-2016 -9962 重新-出现在较旧的内核中。 // 注意:核心问题本身(传入)g dirfds 到主机文件系统) // 已解决。 // https://github.com/torvalds/linux/blob/v4.9/fs/exec.c#L1290-L1318 unix.Close(l .fifoFd) // 设置 seccomp 尽可能接近 execve Masu 。 // 确保随后执行的系统调用更少(减少用户需要在 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") if err := 系统调用。 Exec(name, l.config.Args[0:], os.Environ()); err != nil { return newSystemErrorWithCause(err, "exec user process") } return nil}(1 ),这个函数首先使用l.config.Config.NoNewKeyring,l.config.Console,setupNetwork, setupRoute, label.Init()
(2), if l.config.Config.Namespaces.Contains(configs.NEWNS) -> setupRootfs(l.config.Config, console, l .pipe)
(3)、设置主机名、apparmor.ApplyProfile(...)、label.SetProcessLabel(...)、l.config.Config.Sysctl
( 4)调用remountReadonly(path)重新挂载配置文件中的/proc/asound、/proc/bus、/proc/fs等ReadonlyPath。
(5), maskPath(path ) 设置MaskedPaths,pdeath := system.GetParentDeathSignal(),处理l.config.NoNewPrivileges
(6),调用syncParentReady(l.pipe) ) // 告诉父进程容器可以执行Execv。 看起来创建是从父进程完成的。
(7),处理l.config.Config.Seccomp和l.config.NoNewPrivileges、finalizeNamespace(l.config)、pdeath.Restore()、syscall.Getppid()和l.parentPid是equal,找到名称,执行 err := exec.Lookpath(l.config.Args[0]) 最后执行 l.pipe.Close() 完成 init 。 至此,子进程的创建也完成了。
(8), fd, err := syscall.Openat(l.stateDirFD, execFifoFilename, os.O_WRONLY|syscall.O_CLOEXEC, 0) ---> 在执行用户进程之前等待另一侧的 FIFO 打开。 它实际上就在这里。 然后它等待启动命令并向 fd 写入一个字节以进行同步: syscall.Write(fd, []byte("0"))
(9), syscall(name, l.config. Args[0:], os.Environ()) 执行容器命令
评论前必须登录!
注册