busybox

一个简单的UNIX文件系统,里面很多文件,可以将busybox理解一个文件夹
使用它来作为第一个容器内运行的文件系统。

docker run -d busybox top

docker run -d busybox top 这个命令是用来在Docker中启动一个新的容器并执行特定操作的。下面是对命令各部分的详细解释:

  • docker run: 这是Docker的主要命令之一,用于从指定的镜像启动一个新的容器。

  • -d: 这是一个标志(flag),它告诉Docker以守护进程(detached)模式运行容器。这意味着容器将在后台运行,而不是直接连接到用户的终端。当容器以这种方式运行时,Docker会立即返回容器ID,用户可以继续在命令行中执行其他命令而不影响容器的运行。

  • busybox: 这是镜像的名称,BusyBox是一个轻量级的类Unix工具集合,包含了大量常用命令行工具在一个小巧的镜像中。

  • top: 这是将要在容器内部执行的命令。在Linux系统中,top 命令提供了一个实时的系统状态报告,显示了当前系统中各进程的资源占用情况,比如CPU和内存使用率等。

综上所述,当你执行 docker run -d busybox top 这个命令时,Docker会从BusyBox镜像创建一个新的容器并在该容器内部启动 top 命令,使其成为一个长期运行的进程,从而使得容器持续在后台运行。这种做法常用于测试或者监控目的,让容器内的 top 命令持续展示容器内部的系统状态。

containerId=(docker ps --filter "ancestor=busybox:latest"|grep -v IMAGE|awk '{print $1}')

containerId=(docker ps --filter "ancestor=busybox:latest"|grep -v IMAGE|awk '{print $1}') 这行Shell命令的目的是为了获取基于busybox:latest镜像启动的最新容器ID,并将其赋值给变量containerId。下面逐一分解说明:

  • docker ps: 这是Docker的命令,用于列出所有正在运行的容器及其相关信息。

  • --filter "ancestor=busybox:latest": 这是一个过滤器参数,它会筛选出那些从busybox:latest镜像派生出来的容器。

  • | (管道符号):用于将前面命令的输出作为后面命令的输入。

  • grep -v IMAGE: grep命令用于在文本流中查找匹配项,-v参数表示反向选择,即排除包含指定模式的行。这里过滤掉了输出中描述镜像的那一行,因为我们只需要容器ID。

  • awk '{print $1}': awk是一个强大的文本分析工具,这里使用它提取每行的第一列数据,即容器ID列。

awk '{print $1}' 是一种基于 awk 工具的命令行表达式,用于处理文本数据流并对数据进行操作。在这条命令中:

  • 它读取文件或者输入流(在此案例中是通过管道传递过来的 docker ps 的输出),并按照指定的模式对每一行数据进行处理。

  • {} 中括号内包含了awk命令要执行的动作。

  • print 是awk内置的一个命令,用于输出指定的字段或变量。

  • $1 是awk中的特殊变量,表示每一行记录的第一个字段(字段之间默认由空格或制表符等空白字符分隔)。在许多UNIX/Linux命令的输出格式中,第一列常常包含关键信息,比如在 docker ps 输出中,第一列通常是容器ID。

综合以上命令,它实际上是从正在运行的容器列表中找出最近基于busybox:latest镜像启动的容器,并取出它的ID。然后将这个ID值赋给shell脚本中的变量containerId

最后的 echo "containerId" $containerId 是用来输出变量containerId的值,可以看到实际获得的容器ID。

docker export -o busybox.tar $containerId or sudo docker export 09bbf421d93f > ./busybox.tar

docker export -o busybox.tar $containerId 是一个Docker命令,用于将指定容器的文件系统内容导出为一个tar归档文件。

  • docker export: 这是Docker命令,用于导出容器的文件系统快照。这个命令只会导出容器内的文件系统层次结构,不包括容器的元数据、配置信息或运行时状态。

  • -o busybox.tar: -o 选项后跟的是输出文件的名称,这里是指定将导出的内容保存到名为 busybox.tar 的tar文件中。

  • $containerId: 这是一个变量,代表之前通过命令获取的容器ID。在这个命令中,它指定了要导出的容器的具体实例。

所以,整个命令的意思是:从之前通过 docker run 命令创建并运行的BusyBox容器中,以tar文件的形式导出其文件系统内容,并将导出的文件命名为 busybox.tar。这个tar文件包含了容器内部的所有文件和目录结构,可以用于备份或者在另一个环境中重新导入这个文件系统。

tar -xvf busybox.tar -C busybox/

tar -xvf busybox.tar -C busybox/ 这条命令是用来解压缩并提取 busybox.tar 文件中的内容到指定的目录下的。命令各部分的含义如下:

  • tar: 这是一个在Unix/Linux系统中用于处理档案文件(通常是*.tar文件)的工具,可以用来打包、压缩、解压和列出档案内的文件。

  • -x: 这个选项表示执行解压操作(extract)。它告诉tar工具从指定的tar档案中提取文件。

  • -v: 这是verbose模式,解压过程中会显示详细信息,包括正在解压的文件名,方便用户了解解压进度。

  • -f: 这个选项后面跟着的是要操作的tar文件名,即 busybox.tar。这个参数是必需的,用来指定你想要解压的tar归档文件。

  • -C: 这个选项允许指定解压后文件存放的目录,其后面跟的是目标目录路径 busybox/。这意味着所有从 busybox.tar 中解压出来的文件和目录都将被放在名为 busybox 的目录下。

综上所述,这条命令的整体效果是从 busybox.tar 中提取所有文件,并将它们解压到当前目录下的 busybox/ 子目录中。如果 busybox/ 目录尚不存在,tar 会在解压前创建这个目录。
从零自制docker-11-【pivotRoot切换实现文件系统隔离】-LMLPHP

mount namespace的共享性

在Linux的mount namespace中挂载一个文件系统,是否会影响宿主机的文件系统视具体情况而定。

  • 默认情况下(在systemd引入之前或设置了MS_SHARED传播类型时)
    如果在新的mount namespace中挂载文件系统,且挂载点是共享的(即挂载传播类型为MS_SHARED),那么在新的mount namespace中的挂载操作将会传播回宿主机以及其他共享该挂载点的mount namespace。这意味着宿主机的文件系统视图会发生相应的变化。

  • 设置为私有挂载(MS_PRIVATE)
    若在创建mount namespace时,将挂载点设为私有(通过syscall.Mount("", "/", "", syscall.MS_PRIVATE|syscall.MS_REC, "")命令),那么在这个新的mount namespace中挂载的任何文件系统,将不会影响宿主机或其他未设置为私有的mount namespace。这意味着在新的mount namespace中对文件系统的挂载、卸载操作,只会对该命名空间产生影响,宿主机的文件系统视图将维持不变。

在容器技术如Docker中,通常会创建一个新的mount namespace并将其内部的挂载点设为私有,以确保容器内部的文件系统操作不会影响到宿主机或其他容器。这就实现了容器级别的文件系统隔离。

自我挂载

想象一下,你有一棵大树,树干代表宿主机的根文件系统,树枝代表挂载在其上的各个目录。当你想要创建一个独立的分支(比如 /newroot)作为容器的新根目录时,如果不采取措施,这个分支仍会受到整棵树(宿主机)上其他变动的影响。

例子
假设在宿主机上有一个目录结构如下:

/
├── bin
├── etc
├── home
├── newroot
│   ├── bin
│   ├── etc
│   └── var
└── var

你想让 /newroot 成为容器的独立根目录,但如果不对 /newroot 进行自我绑定挂载,那么在 /newroot 下做的挂载操作会影响到宿主机的其他部分。例如,如果在 /newroot/var 下挂载了一个临时文件系统,这个挂载也将传播到宿主机的 /var 目录。

然而,通过执行自我绑定挂载(mount --bind /newroot /newrootsyscall.Mount("/newroot", "/newroot", "", syscall.MS_BIND|syscall.MS_REC, "")),你实际上创建了一个“镜像”分支,这个分支与宿主机的其余部分在挂载行为上是相互独立的。此后,在 /newroot 下做的任何挂载操作,比如挂载临时文件系统到 /newroot/var,将只影响 /newroot 这个“镜像”分支,而不会影响到宿主机原有的 /var 目录。

这样一来,在执行 pivot_root/newroot 切换为容器的新根目录时,新的根目录 (/newroot) 就是一个清洁且独立的文件系统视图,它与宿主机的其余部分互不影响,从而实现了容器所需的隔离性。

syscall.Mount("", "/", "", syscall.MS_PRIVATE|syscall.MS_REC, "")

syscall.Mount("", "/", "", syscall.MS_PRIVATE|syscall.MS_REC, "") 是一个在Go编程语言中调用Linux内核 mount() 系统调用的代码片段,目的是改变当前进程的根目录挂载点的挂载标志。具体来说:

  • 第一个参数 "" 表示当前挂载点,这里为空字符串表示当前进程的根目录 /
  • 第二个参数也是 "/",指的是目标挂载点,这里也是根目录。
  • 第三个参数 "" 表示文件系统类型,由于这不是挂载一个新的文件系统,而是修改现有挂载点的属性,所以这里也留空。
  • 第四个参数是挂载标志,syscall.MS_PRIVATE|syscall.MS_REC
    • syscall.MS_PRIVATE:设置当前挂载点及其所有子挂载点为私有挂载。这意味着在这个挂载点上进行的任何挂载、卸载或重新挂载操作,都不会传播到其他挂载命名空间,从而实现了挂载事件的隔离,这对于创建独立的容器环境或其他需要文件系统隔离的场景非常关键。
    • syscall.MS_REC:这是一个递归标志,意味着这个私有挂载属性不仅应用到根目录 / 上,还将应用到根目录下的所有子挂载点,确保整个挂载树都变为私有挂载。
  • 第五个参数 "" 表示额外的挂载选项参数,这里为空字符串,表明没有额外的挂载选项需要设置。

综上所述,这条命令的作用是将当前进程的整个文件系统挂载树(包括根目录和所有子目录)设置为私有挂载,确保在当前进程及其后代进程的挂载命名空间内进行的挂载操作不会影响到宿主机或其他进程的命名空间。

bind mount

这是因为 “bind mount” 操作会在宿主机上创建一个新的文件系统挂载点,该挂载点与原来的 /container_root 目录具有相同的内容,但位于一个不同的文件系统中。

具体来说:

  1. 在容器中,/container_root 是容器的根文件系统。
  2. 当我们执行 mount --bind /container_root /container_root 时,会在宿主机上创建一个新的文件系统挂载点,该挂载点指向 /container_root 目录。
  3. 这个新的文件系统挂载点与宿主机的根文件系统 /host_root 是不同的文件系统。

也就是说,从宿主机的角度来看,/container_root 现在是一个独立的文件系统,与宿主机的根文件系统 /host_root 不在同一个文件系统中。

这个过程可以用一个例子来解释:

假设在宿主机上,/container_root 原本是宿主机根文件系统 /host_root 的一个子目录。但是执行 mount --bind 后,/container_root 就变成了一个独立的文件系统挂载点,与 /host_root 不在同一个文件系统中了。

这样做的目的就是为了满足 syscall.PivotRoot 函数的要求,即新的根文件系统和旧的根文件系统不能在同一个文件系统下。通过 “bind mount” 操作,我们可以实现这个要求。

挂载点的私有和mount namespace的私有

Mount Namespace的属性主要指的是挂载点在Namespace中的传播属性(Mount Propagation)。下面通过一个例子来解释:

例子场景
假设在宿主机上有两个目录/mnt/shared/mnt/private,并且宿主机的根目录/的挂载传播属性为默认值。

  1. 创建新的Mount Namespace并设置挂载点传播属性
    我们可以使用unshare命令创建一个新的Mount Namespace,并将/mnt/shared设置为共享挂载(shared)属性,将/mnt/private设置为私有挂载(private)属性。

    sudo unshare -m bash            # 创建一个新的Mount Namespace
    sudo mount --make-shared /mnt/shared    # 设置/mnt/shared为共享挂载
    sudo mount --make-private /mnt/private  # 设置/mnt/private为私有挂载
    
  2. Namespace内的操作

    • 共享挂载(/mnt/shared):在新的Namespace中,如果挂载了一个新的文件系统到/mnt/shared/subdir,由于/mnt/shared是共享挂载,所以在宿主机或其他Mount Namespace中也可以看到这个新增的挂载点。
    • 私有挂载(/mnt/private):如果在新的Namespace中对/mnt/private做任何挂载或卸载操作,这些操作不会影响到宿主机或其他Mount Namespace。也就是说,在新的Namespace中对/mnt/private的任何更改对外部Namespace都是透明的。
  3. 效果

    • 新Namespace内的进程只能看到在该Namespace内部定义的挂载点和它们的传播属性。
    • 对于/mnt/shared,在Namespace内外的挂载操作会相互影响,而对于/mnt/private,则完全隔离。

通过这个例子,我们可以看出Mount Namespace的属性是如何影响挂载点在不同Namespace间的可见性和操作效果的。在容器技术中,Mount Namespace的这些属性为容器提供了独立的文件系统视图,有助于实现资源隔离和安全性。

pivotroot

会切换根系统目录,但如果不会自动切换当前工作目录到跟系统去。如果不切换,那么会依然停留到执行该程序的工作目录
如下图
从零自制docker-11-【pivotRoot切换实现文件系统隔离】-LMLPHP
加个syscall.Chdir("/");
从零自制docker-11-【pivotRoot切换实现文件系统隔离】-LMLPHP
不加syscall.Chdir("/");,但加个command.Dir = "/home/llk/Desktop/docker/src/pivotroot_docker/busybox"
那么当前的工作目录在command.start会自动切换
从零自制docker-11-【pivotRoot切换实现文件系统隔离】-LMLPHP

自己理解

  1. 不同mount namspace有各种的符合当前的namespace的挂载点列表会,就是挂载后会显示的文件系统不同,但文件视图依然和从宿主机继承的一样。而是否挂载后会影响到宿主机和其他mount namespace的文件就看这个挂载点的属性 。如果是私有的就会修改到宿主机和其他mount namespace的文件,否则就不会影响到

下图是将当前进程根目录挂载点设置为私有和在mount namespace中直接启动后,挂载proc到当前容器内的/proc,发现此时并没有覆盖原主机上的/proc
从零自制docker-11-【pivotRoot切换实现文件系统隔离】-LMLPHP
而如果不将当前进程根目录挂载点设置为私有。挂载后的内容会影响到宿主机的/proc。
从零自制docker-11-【pivotRoot切换实现文件系统隔离】-LMLPHP
2. 将当前进程的根目录挂载点下都设置为私有,那么当该进程操作操作该挂载点时,不会影响宿主机和其他mount namespace的根目录挂载点
3. 将新的根目录挂载点绑定挂载,是因为pivotroot需要新的根目录和原根目录所在不同的挂载点。由于新根目录肯定在原根目录的挂载点中,为了使得二者不在同一个挂载点中,于是重复绑定,那么会使得当前进程认为新根目录所在的挂载点在新根目录。这样就使得二者不在同一个挂载点中了

代码

https://github.com/FULLK/llkdocker/tree/main/pivotroot_docker

结果

从零自制docker-11-【pivotRoot切换实现文件系统隔离】-LMLPHP

04-21 07:46