千家信息网

docker中run的示例分析

发表于:2025-01-30 作者:千家信息网编辑
千家信息网最后更新 2025年01月30日,这篇文章给大家分享的是有关docker中run的示例分析的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。docker源码相关通过在/components/cli/comman
千家信息网最后更新 2025年01月30日docker中run的示例分析

这篇文章给大家分享的是有关docker中run的示例分析的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。

docker源码相关

通过在/components/cli/command/commands.go里,抽象出各种命令的初始化操作。

使用第三方库"github.com/spf13/cobra"

  1. docker run 初始化命令行终端解析参数,最终生成 APIclient发出REQUEST请求给docker daemon.

    • docker daemon的初始化,设置了server的监听地址,初始化化routerSwapper, registryService 以及layStore、imageStore、volumeStore等各种存储 。

  2. docker run的命令解析为 docker container create 和 container start 两次请求:

    • 其中container create 不涉及底层containerd的调用,首先将host.config 、networkingConfig和AdjustCPUShares等组装成一个客户端请求,发送到docker daemon注册该容器。该请求会完成拉取image, 以及初始化 baseContainer的RWlayer, config文件等,之后daemon就可以通过containerid来使用该容器。

    • container start 命令的核心是调用了daemon的containerStart(),它会完成

    • 调用containerd进行create容器,调用libcontainerd模块 clnt *client 的初始化,

    1. 设置容器文件系统,挂载点: /var/lib/docker/overlay/{container.RWLayer.mountID}/merged

    2. 设置容器的网络模式,调用libnetwork ,CNM模型(sandbox, endpoint,network)

    3. 创建/proc /dev等spec文件,对容器所特有的属性进行设置,

    4. 调用containerd进行create容器

container.create
1)获取libcontainerd模块中的containers2)获取gid和uid3)创建state目录,配置文件路径。4)创建一个containercommon对象,创建容器目录,以及配置文件路径,根据spec创建配置文件。
container.start
1) 读取spec对象的配置文件2) 创建一个fifo的pipe3) 定义containerd的请求对象,grpc调用containerd模块。ctr.client.remote.apiClient.CreateContainer(context.Background(), r)4)启动成功后,更新容器状态。
daemon 启动libcontainerd ,作为grpc的server。
  1. cmd/dockerd/daemon.go 中存在libcontainerd初始化的流程。

    包括启动grpc服务器,对套接字进行监听。

    通过grpc.dail 与grpc server建立连接conn, 根据该链接建立apiclient对象,发送json请求。

  2. runContainerdDaemon

    通过docker-containerd二进制与grpc server进行通信,

    docker-containerd -l unix:///var/run/docker/libcontainerd/docker-containerd.sock --metrics-interval=0 --start-timeout 2m --state-dir /var/run/docker/libcontainerd/containerd --shim docker-containerd-shim --runtime docker-runc

    执行结果的输入输出流重定向到docker daemon。

     runc把state.json文件保存在容器运行时的状态信息,默认存放在/run/runc/{containerID}/state.json。


containerd源码相关

type Supervisor struct {        // stateDir is the directory on the system to store container runtime state information.        stateDir string        // name of the OCI compatible runtime used to execute containers        runtime     string        runtimeArgs []string        shim        string        containers  map[string]*containerInfo        startTasks  chan *startTask //这是containerd到runc的桥梁,由func (w *worker) Start()消费        // we need a lock around the subscribers map only because additions and deletions from        // the map are via the API so we cannot really control the concurrency        subscriberLock sync.RWMutex        subscribers    map[chan Event]struct{}        machine        Machine        tasks          chan Task //所有来自于docker-daemon的request都会转化为event存放到这,由func (s *Supervisor) Start()消费        monitor        *Monitor        eventLog       []Event        eventLock      sync.Mutex        timeout        time.Duration}type startTask struct {        Container      runtime.Container        CheckpointPath string        Stdin          string        Stdout         string        Stderr         string        Err            chan error        StartResponse  chan StartResponse}

我们知道containerd作为docker daemon的grpc server端,通过接收 apiclient request转化成对应的events,在不同的子系统distribution , bundles , runtime 中进行数据的流转,包括镜像上传下载,镜像打包和解压,运行时的创建销毁等。

其中containerd 核心组件包括 supervisor 和executor, 数据流如下:

docker-daemon--->tasks chan Task --->func (s *Supervisor) Start()消费   --->存放到startTasks  chan *startTask      -->func (w *worker) Start()消费

containerd的初始化

docker-containerd初始化包括 新建Supervisor对象:

  1. 该对象会启动10个worker,负责处理创建新容器的任务(task)。

  2. supervisor的初始化,包括startTask chan初始化,启动监控容器进程的monitor

  3. 一个worker包含一个supervisor和sync.waitgroup,wg用于实现容器启动。

  4. supervisor的start,消费tasks,把task中的container数据组装成runtime.container, 封装到type startTask struct,发送到startTask chan队列。

  5. 启动grpc server(startServer),用来接收dockerd的request请求。

func daemon(context *cli.Context) error {        s := make(chan os.Signal, 2048)        signal.Notify(s, syscall.SIGTERM, syscall.SIGINT)        /*                新建一个supervisor,这个是containerd的核心部件                        ==>/supervisor/supervisor.go                                ==>func New        */        sv, err := supervisor.New(                context.String("state-dir"),                context.String("runtime"),                context.String("shim"),                context.StringSlice("runtime-args"),                context.Duration("start-timeout"),                context.Int("retain-count"))        if err != nil {                return err        }        wg := &sync.WaitGroup{}        /*                supervisor 启动10个worker                        ==>/supervisor/worker.go        */        for i := 0; i < 10; i++ {                wg.Add(1)                w := supervisor.NewWorker(sv, wg)                go w.Start()        }        //启动supervisor        if err := sv.Start(); err != nil {                return err        }    // Split the listen string of the form proto://addr        /*                根据参数获取监听器                listenSpec的值为 unix:///var/run/docker/libcontainerd/docker-containerd.sock        */        listenSpec := context.String("listen")        listenParts := strings.SplitN(listenSpec, "://", 2)        if len(listenParts) != 2 {                return fmt.Errorf("bad listen address format %s, expected proto://address", listenSpec)        }        /*                启动grpc server端        */        server, err := startServer(listenParts[0], listenParts[1], sv)        if err != nil {                return err        }

其中startServer负责启动grpc server,监听docker-containerd.sock,声明注册路由handler。

  1. 当CreateContainer handler接收到一个Request之后,会把其转化成type startTask struct,将其转化为一个StartTask 事件,其中存放创建容器的request信息。

  2. 通过s.sv.SendTask(e)将该事件发送给supervosior 主循环。

// SendTask sends the provided event to the the supervisors main event loop/*        SendTask将evt Task发送给 the supervisors main event loop        所有来自于docker-daemon的request都会转化为event存放到这,生产者*/func (s *Supervisor) SendTask(evt Task) {        TasksCounter.Inc(1) //任务数+1        s.tasks <- evt}
  1. 等待woker.Start()消费处理结果后,将StartResponse返回给docker-daemon。

supervisor.start

负责将每一个request转化成特定的task类型,通过一个goroutine遍历task中所有的任务并进行处理。消费tasks,把task中的container数据组装成runtime.container, 封装到type startTask struct,发送到startTask chan队列。

worker.start

负责调用containerd-shim, 监控容器中的进程,并把结果返回给StartResponse chan队列。

其中,

  1. container.Start() 通过containerd-shim 调用runc create {containerID}创建容器。

     process, err := t.Container.Start(t.CheckpointPath, runtime.NewStdio(t.Stdin, t.Stdout, t.Stderr)) 其中值得注意的是,container.start 和container.exec均是调用createcmd,exec 命令则是通过process.json中的相关属性来判断是Start()还是Exec(),最后组装成containerd-shim的调用命令。 当具体容器内进程pid生成(由runc生成)后,createCmd会启动一个go routine来等待shim命令的结束。 shim命令一般不会退出。 当shim发生退出时,如果容器内的进程仍在运行,则需要把该进程杀死;如果容器内进程已经不存在,则无需清理工作。


  2. process.Start() 通过调用runc start {containerID}命令启动容器的init进程

root@idc-gz:/var/run/docker/libcontainerd# tree -L 2 eb347b7e27ecbc01f009971a13cb1b24a89baad795f703053de26d9722129039/eb347b7e27ecbc01f009971a13cb1b24a89baad795f703053de26d9722129039/├── 95de4070f528e1d68c80142f679013815a2d1a00da7858c390ad4895b8f8991b-stdin├── 95de4070f528e1d68c80142f679013815a2d1a00da7858c390ad4895b8f8991b-stdout├── config.json├── dc172589265f782a476af1ed302d3178887d078c737ff3d18b930cbc143e5fd5-stdin├── dc172589265f782a476af1ed302d3178887d078c737ff3d18b930cbc143e5fd5-stdout├── ef00cfa54bf014e3f732af3bda1f667c9b0f79c0d865f099b1bee014f0834844-stdin├── ef00cfa54bf014e3f732af3bda1f667c9b0f79c0d865f099b1bee014f0834844-stdout├── init-stdin└── init-stdoutroot@idc-gz:/var/run/docker/libcontainerdcontainerd# tree -L 2 eb347b7e27ecbc01f009971a13cb1b24a89baad795f703053de26d9722129039/eb347b7e27ecbc01f009971a13cb1b24a89baad795f703053de26d9722129039/├── dc172589265f782a476af1ed302d3178887d078c737ff3d18b930cbc143e5fd5│   ├── control│   ├── exit│   ├── log.json│   ├── pid│   ├── process.json│   ├── shim-log.json│   └── starttime├── ef00cfa54bf014e3f732af3bda1f667c9b0f79c0d865f099b1bee014f0834844│   ├── control│   ├── exit│   ├── log.json│   ├── pid│   ├── process.json│   ├── shim-log.json│   └── starttime├── init│   ├── control│   ├── exit│   ├── log.json│   ├── pid│   ├── process.json│   ├── shim-log.json│   └── starttime└── state.json

runc源码相关

runc create

在源码create.go中,首先会加载config.json的配置,然后调用startContainer函数,其流程包括:

  1. createContainer, 生成libcontainer.Container对象,状态处于stopped、destoryed。

    • 调用loadFactory方法, 生成一个libcontainer.Factory对象。

    • 调用factory.Create()方法,生成libcontainer.Container

  2. 把libcontainer.Container封装到type runner struct对象中。

    • runner.run负责将config.json设置将来在容器中启动的process,设置iopipe和tty

    • runc create ,调用container.Start(process)

    1. linuxContainer.newParentPorcess组装要执行的parent命令, 组装出来的命令是/proc/self/exe init, 通过匿名管道让runc create 和runc init进行通信。

    2. parent.start()会根据parent的类型来选择对应的start(),自此之后,将进入/proc/self/exe init,也就是runc init

    3. 将容器状态持久化到state.json,此时容器状态为created.

  3. runc start,调用container.Run(process)

// LinuxFactory implements the default factory interface for linux based systems.type LinuxFactory struct {        // Root directory for the factory to store state.        /*                factory 存放数据的根目录  默认是 /run/runc                而/run/runc/{containerID} 目录下,会有两个文件:          一个是管道exec.fifo                  一个是state.json        */        Root string        // InitArgs are arguments for calling the init responsibilities for spawning        // a container.        /*                用于设置 init命令 ,固定是 InitArgs:  []string{"/proc/self/exe", "init"},        */        InitArgs []string        // CriuPath is the path to the criu binary used for checkpoint and restore of        // containers.        // 用于checkpoint and restore        CriuPath string        // Validator provides validation to container configurations.        Validator validate.Validator        // NewCgroupsManager returns an initialized cgroups manager for a single container.        // 初始化一个针对单个容器的cgroups manager        NewCgroupsManager func(config *configs.Cgroup, paths map[string]string) cgroups.Manager}// 一个容器负责对应一个runnertype runner struct {        enableSubreaper bool        shouldDestroy   bool        detach          bool        listenFDs       []*os.File        pidFile         string        console         string        container       libcontainer.Container        create          bool}

runc init

runc create clone出一个子进程,namespace与父进程隔离,子进程中调用/proc/self/exe init进行初始化。

runc init的过程如下:

  1. 调用factory.StartInitialization();

    1. 配置容器内部网络,路由,初始化mount namespace, 调用setupRootfs在新的mount namespaces中配置设备、挂载点以及文件系统。

    2. 配置hostname, apparmor,processLabel,sysctl, readyonlyPath, maskPath.

    3. 获取父进程的退出信号,通过管道与父进程同步,先发出procReady再等待procRun

    4. 恢复parent进程的death信号量并检查当前父进程pid是否为我们原来记录的不是的话,kill ourself。

    5. 与父进程之间的同步已经完成,关闭pipe。

    6. "只写" 方式打开fifo管道并写入0,会一直保持阻塞。等待runc start以只读的方式打开FIFO管道,阻塞才会消除。之后本进程才会继续执行。

    7. 调用syscall.Exec,执行用户真正希望执行的命令。用来覆盖掉PID为1的Init进程。至此,在容器内部PID为1的进程才是用户希望一直在前台执行的进程。

    8. init进程通过匿名管理读取父进程的信息,initType以及config信息。

    9. 调用func newContainerInit(),生成一个type linuxStandardInit struct对象

    10. 执行linuxStandardInit.Init(),Init进程会根据config配置初始化seccomp,并调用syscall.Exec执行cmd。

runc start

runc start的逻辑比较简单,分为两步:

  1. 从context中获取libcontainer.container对象。

  2. 通过判断container 的状态为created,执行linuxContainer.exec()。

  • 以"只读"的方式打开FIFO管道,读取内容。这同时也恢复之前处于阻塞状态的`runc Init`进程,Init进程会执行最后调用用户期待的cmd部分。

  • 如果读取到的data长度大于0,则读取到Create流程中最后写入的"0",则删除FIFO管道文件。

感谢各位的阅读!关于"docker中run的示例分析"这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,让大家可以学到更多知识,如果觉得文章不错,可以把它分享出去让更多的人看到吧!

0