浅入浅出docker run命令源码

1、背景

源码阅读环境搭好了,怎么也得看看源码。对docker的最深认识就是docker是一个配置了不同namespace的进程,其余一概不懂。让我来看看docker run命令到底干了啥,顺便学习下go,
为捣鼓k8s源码打打基础。

2、摘要

docker run整个命令的执行过程中,cli会给守护进程dockerd发送两个HTTP请求,分别是/containers/create以及/containers/{name:.*}/start

/containers/create请求,dockerd进程主要是准备好容器启动所需要文件系统rootfs以及dockerd管理容器所需要的一些元数据对象,然后返回容器ID给clicli收到回复后,会再发送一个/containers/{name:.*}/start来启动容器,这里参数name就是前面返回的容器ID。

/containers/{name:.*}/start请求,dockerd进程继续完善容器的配置信息,例如网络相关的配置,随后dockerd进程会通过client将运行所需要相关参数发送给containerd进程,由containerd进程调用runc完成容器的创建。

3、代码入口

如何知道docker run命令对应的API是什么,以及对应的处理逻辑代码入口在哪里,请看我前面的文章《了解docker架构》。

/containers/create请求以及 /containers/{name:.*}/start 对应的代码在
在这里插入图片描述

以下面命令为示例,进行部分docker源码逻辑说明

./build/docker run --rm --name test hello-world

4、/containers/create请求源码阅读

想要大致读懂这部分的代码,需要掌握一些基础的知识
1、当我们执行ls命令时,我们看到的目录是怎么来的
2、为什么容器里面有自己独立的一套目录
3、什么是overlayFS, docker与overlayFS是什么关系

由于我也只是了解一点点,只能简单说下我的浅薄理解,错了的话,那就没办法了,只能说尽力了

4.1 ls命令输出的目录是从哪里来的?

我没看过linux的源码,但前一段时间跟着《操作系统真象还原》一书学习写了个小型操作系统。里面有参考ext2的思路实现了一个简化的文件系统。

在操作系统层面,在内存某个位置记录了根目录信息。所谓的根目录也就是平时大家说的/,根目录信息就是,目录/下有哪些文件以及目录。当我们要查找文件的时候,例如执行 ls命令的时候就是先找到根目录/然后递归查找下去。找到当前目录,然后把当前目录对应的目录信息查询出来。

这就是ls命令输出的原理。

4.2 容器怎么有自己独立的一套目录?

前面知道根目录是操作系统维护的,ls命令以及输出结果也是操作系统提供的。当你在容器里面
执行ls /命令的时候,操作系统,不去读取真正的目录,而是返回提前准备好的某个目录的的内容,而这个提前准备好的目录的内容和真正的根目录的内容一样呢?是不是就好像返回了真正的根目录一样。

如果你执行写文件、删除文件时候,操作系统操作的就是这个提前的目录,而不是真正的目录。这样子,容器的所有操作影响的只是这提前准备好的目录,就达到了容器拥有自己独立的一套目录的效果了。

前面是我粗浅的了解,本质是利用了linux的 mount namespace机制,直接将容器的根目录设置为docker提前准备好的假的根目录。这个在极客时间里面张磊大神 的《深入剖析kubernetes》的第7课深入理解容器镜像 里面有说。

4.3 OverlayFS与docker是什么关系?

前面说的,docker准备好了个假的根目录,这个目录是基于OverlayFS文件系统构建的。

OverlayFS 是 Linux 系统中的一种联合文件系统(UnionFS),它能将多个目录合并在一起,形成一个虚拟视图,对外看起来就像是一个目录。这玩意有什么用呢?

例如我现在有两个目录 A 和 B, A作为底层目录是只读的,B作为上层目录是可写的,B目录中如果如果有文件或者目录在A中也有,则以B中的为准。这个时候就可以用下面的命令得到一个C目录

sudo mount -t overlay overlay -o lowerdir=/path/to/A,upperdir=/path/to/B,workdir=/path/to/workdir /path/to/C

C目录里面会有AB目录中所有内容,当我们读取C目录的时候,OverlayFS 会优先查看 B中的文件。如果文件存在,返回该文件;如果文件在 B 中不存在,则返回 A中的文件;当我们要对 C 中的文件进行修改时,修改会写入到 B中,而 A不受影响。

如果我又用AD搞一个E出来呢?是不是就做到了 我们都有相同的A,且互不影响的情况下,复用了 A,减少了空间的占用。

这就是docker镜像的分层复用原理,在docker启动容器的时候,把镜像的文件夹作为只读的A目录,然后属于容器自己变化了的文件放在B目录来达到,然后用OverlayFS生成虚拟视图目录作为容器的根目录,从而达到容器文件目录独立的效果的。

4.4源码阅读笔记

/containers/create 用于创建容器。但不会立即启动容器,只是完成容器的初始化配置,
例如你在容器里面看到的绝大部分文件以及文件夹,也就是rootfs。为什么说是绝大部分呢,因为
还有一些是在后面那个请求里面创建的。

最外层接口的代码很清晰,一开始会执行一大堆的配置解析与设置,以及版本参数校验等等,然后在最后,调用Daemon.containerCreate函数

这个函数会去查询镜像是否存在,从实际执行效果来看,如果镜像不存在应该会触发pull 操作的,按照docker api的风格来看,应该是返回错误,然后由cli触发pull操作。这个不是我想看的逻辑,且我镜像已经存在所以跳过了。

if opts.params.Platform == nil && opts.params.Config.Image != "" {  
    img, err := daemon.imageService.GetImage(ctx, opts.params.Config.Image, backend.GetImageOpts{Platform: opts.params.Platform})  
    if err != nil {  
       return containertypes.CreateResponse{}, err  
    }  
    if img != nil {  
		...省略
    }  
}

找到镜像后接着又做了一些校验后才会,调用create函数去创建Contianer对象,核心逻辑入口在这,创建完成后就会返回容器的ID给client
在这里插入图片描述

这个 Container 结构体是 Docker 源代码中用来表示容器的核心数据结构之一。它包含了容器的各种信息,包括容器的配置、网络设置、存储层等。

type Container struct {
    StreamConfig *stream.Config  // 配置流的设置,如容器的日志流等
    *State          `json:"State"`  // 容器的状态信息,使用嵌入结构体(State),容器直接支持状态,并且在 JSON 中作为一个结构体序列化。

    Root            string        `json:"-"`  // 容器的“家”目录路径,包括元数据。`json:"-"` 表示该字段不会被序列化成 JSON。
    BaseFS          string        `json:"-"`  // 图层驱动挂载点路径,通常用于容器的文件系统。
    RWLayer         layer.RWLayer `json:"-"`  // 只读写层,用于容器的文件存储。
    
    ID              string    // 容器的唯一标识符。
    Created         time.Time // 容器创建时间。
    Managed         bool      // 是否由 Docker 管理。
    Path            string    // 容器启动时的路径。
    Args            []string  // 启动容器时传递的参数。
    Config          *containertypes.Config // 容器的配置(如环境变量、挂载路径等)。
    ImageID         image.ID `json:"Image"`  // 容器使用的镜像 ID。
    ImageManifest   *ocispec.Descriptor // 容器镜像的描述符,提供镜像元数据。
    NetworkSettings *network.Settings // 容器的网络设置。
    LogPath         string    // 容器日志的路径。
    Name            string    // 容器名称。
    Driver          string    // 容器使用的存储驱动。
    MountPoints              map[string]*volumemounts.MountPoint // 容器的挂载点。
    ... 省略
}

这个结构体集合了容器运行时的各种状态、配置和文件系统信息, 是Docker守护进程用来管理容器的生命周期和操作用的。

daemon.Create函数中,就会遇到该接口重点逻辑代码了
在这里插入图片描述

然后又会调用layStore.CreateRWLayer
在这里插入图片描述

在这里,因为我没怎么看过docker相关源码的文章,这是我自己理解的,可能不太对,但我个人
带着这几个概念去看的话,就容易很多:

1、镜像其实是打包好的一堆文件,相关的操作是通过imageService来操作的
2、镜像在imageService是中是按层存储的,层逻辑概念是layer,物理概念是一个文件夹。通俗理解就是每个镜像都有对应的一个文件夹。
3、容器是在镜像的基础上构建的,也是按层的,但叫mountedLayer,所以也归imageService管
4、imageService并不是直接处理layer的,而是让它的手下layerStore来执行layer相关的操作的

下面是layStore.CreateRWLayer中逻辑
在这里插入图片描述

顾名思义,是创建一个可写层,创建流程分为3步
1、创建init层
2、以init层为基准创建容器目录
3、保存创建的层信息
以下是各个步骤中的说明

1、创建init层

在这里插入图片描述

ls.initMount函数会先创建一个init层(因为文件夹是以容器ID-init这样命名的,我不知道怎么称呼),init文件夹里面会创建,work目录、diff目录、lower文件、link文件

work目录就是使用linux mount api的时候,挂载overlayfs时要指定的work目录;diff目录对应的是upper目录;

lower中则是镜像对应的文件夹的软连接,就是挂载overlayfs时指定的lower目录
link文件则是记录了diff文件夹的软链接路径,就是挂载overlayfs时指定的upper目录

docker创建的软链接都放在/var/lib/docker/overlay2/l目录下,为什么这里要用软链接而不是直接使用绝对路径,从后续的校验逻辑来看,我猜是OverlayFS在实现的时候,会现申请一个页的大小来做工作空间,用来存放这个lower的路径字符串,如果这个字符串太长就会报错。

还有两个函数需要说明一下,分别是ls.driver.Get,ls.driver.Put
Get函数是调用linux的mount API挂载overlay目录,Put函数是调用unmout API卸载overlay目录。driver默认是overlay2

创建init层的时候,会先挂载为overlayfs目录,然后又卸载掉,也就是先Get,然后紧接又Put,据说是为了测试挂载是否有潜在的问题。

initMount函数执行完毕,/var/lib/docker/overlay2/{容器id}-init目录就创建好了
此时,目录中会有work目录、diff目录、lower文件、link文件。

2、以init层为基准创建容器层
init层创建完成后,会继续调用ls.driver.CreateReadWrite以init层为父层,创建容器层(同上,不知道叫啥名),文件夹名字为容器ID。所谓的父层就是从init层中的lower文件中的目录 加上link文件中的目录作为 容器层的 lower文件的内容。

其余流程和init层创建的差不多,这里就不再描述。

3、保存创建的层信息
这步逻辑就是将id写到mount目录下,保存元数据信息,用于管理,比较容易理解就不再描述

三个步骤完成后,返回结果给cli前,还会再执行一次mount以及unmout操作,理解了上面的操作之后,就很容易理解了。至此,没啥好看的了。

5、/containers/{name:.*}/start请求源码阅读

该接口的代码相对就容易理解了,因为大部分逻辑都是在校验参数以及初始化一下网络配置文件
真正的启动逻辑在这里
在这里插入图片描述

继续跟下去,你会发现最后调用了grpc请求containerd进程。
在这里插入图片描述

要继续看代码,就得看containerd的源码了,下面是gpt给出的容器启动流程

假设用户通过 docker run 启动一个容器,调用流程大致如下:
1.用户执行 docker run 命令 → dockerd 接收并处理请求。
2.dockerd 将请求转发给 containerd,让 containerd 启动容器。
3.containerd 根据请求使用 runc 启动容器,调用 runc 创建容器并设置相关的内核特性(如命名空间、cgroups 等)。
4.runc 启动容器进程,并将其与宿主机的内核隔离,确保容器能够在独立的环境中运行。

在这里插入图片描述

总结

虽然只是读了些上层代码,但是还是有点收获的,例如知道了dockerd在运行docker run命令到底干了点啥,理解了/var/lib/docker/overlay2目录下的文件到底是干嘛的,了解了docker是如何管理容器的,了解了dockerd是如何与containerd整合的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值