关注微信公众号《云原生CTO》更多云原生干货等你来探索
专注于 云原生技术
分享
提供优质 云原生开发
视频技术培训
面试技巧
,及技术疑难问题 解答

云原生技术分享不仅仅局限于Go
、Rust
、Python
、Istio
、containerd
、CoreDNS
、Envoy
、etcd
、Fluentd
、Harbor
、Helm
、Jaeger
、Kubernetes
、Open Policy Agent
、Prometheus
、Rook
、TiKV
、TUF
、Vitess
、Argo
、Buildpacks
、CloudEvents
、CNI
、Contour
、Cortex
、CRI-O
、Falco
、Flux
、gRPC
、KubeEdge
、Linkerd
、NATS
、Notary
、OpenTracing
、Operator Framework
、SPIFFE
、SPIRE
和 Thanos
等


Rust 中的容器运行时——第一部分

囚禁容器进程
在本系列的第 0 部分中,我们已经看到了进程如何获得它们所看到资源的受限视图。这部分将解释容器运行时如何为容器进程准备和创建隔离环境。
这部分的先决条件是了解 Linux
文件系统的工作原理,什么是 inode
、符号链接
和挂载点
。可以在此处找到这篇文章的完整源代码。
https://github.com/penumbra23/pura
首先,让我们从 OCI
规范开始。
操作
在撰写本文时,OCI
规范至少定义了五种标准操作:创建、启动、状态、删除和终止。记住这一点,使用clap库
我们可以很快地生成一个不错的CLI
界面。它应该是这样的:
clap库 : https://crates.io/crates/clap
let matches = App::new("Container Runtime")
.subcommand(
SubCommand::with_name("create")
.arg(Arg::with_name("bundle").required(true))
.arg(Arg::with_name("id").required(true)),
)
.subcommand(SubCommand::with_name("start").arg(Arg::with_name("id").required(true)))
.subcommand(
SubCommand::with_name("kill")
.arg(Arg::with_name("id").required(true))
.arg(Arg::with_name("signal")),
)
.subcommand(SubCommand::with_name("delete").arg(Arg::with_name("id").required(true)))
.subcommand(SubCommand::with_name("state").arg(Arg::with_name("id").required(true)))
.get_matches();
我们将主要关注create
和start
命令,因为这是运行docker run
命令时最重要的两个命令。
bundle
目录包含配置。Json文件
包含创建容器的所有元数据:
ociVersion - OCI 规范的版本 process - 容器执行的用户定义进程(shell、数据库、Web 应用程序、gRPC 服务等),带有必要的参数和环境变量 root - 容器根目录的子目录路径 容器的主机名 mounts - 容器内的挂载点列表
此外,OCI
规范包含一个特定于平台的部分,支持基于运行容器的平台的自定义设置。因为我们只研究Linux
容器,所以Linux
部分将对我们有用。
create
命令与容器ID
和包路径一起提供。它的目的是初始化容器进程,挂载所有必要的子目录,将容器“监禁”在根目录中。path
文件夹,更新容器内的所有系统变量(env、主机名、用户、组
),执行几个钩子(稍后我们将对此进行研究),为容器本身分配惟一ID
,并等待直到启动start
命令。在create
命令完成后,容器处于已创建状态,用户进程必须等待start
命令来启动实际的容器进程。
关于实现,一切似乎都很简单,但“监禁”的部分可能会有点令人困惑。这是怎么做到的?
Chroot
Chroot
是一个系统调用,它更改调用进程的根目录。它将新的根路径作为参数,它可以是绝对路径或相对路径。来自终端的chroot
命令做同样的事情,除了它需要一个额外的参数,即将在更改的根中执行的进程。
在我们看一个示例之前,首先我们需要准备新的rootfs
。不幸的是,在jail
中使用的二进制文件必须驻留在chroot-ed
目录中(显然),因此我们需要一个预先生成的rootfs
。幸运的是,我们可以使用我们的主机操作系统二进制文件和挂载绑定已经存在的文件,并以这样的结构结束:
Chroot: https://man7.org/linux/man-pages/man2/chroot.2.html

容器文件夹的文件和目录结构
如果您的列表不同,请不要担心,只需确保bin
目录中有bash
和ls
。我们来看看 chroot
命令(使用sudo
运行):

正如我们所看到的,列出根目录之外的目录(ls
..)列出了被监禁的根目录,似乎我们看不到外面的任何东西。此外,列出bin
和lib
目录的结果与上述示例相同。
可以说“这就是容器被监禁的方式”,然后直接从头开始构建容器。但是,事情并没有那么容易…… Chroot
不会更改文件系统,也不会更改进程看到的挂载点。它只是改变了进程根的视图,但一切都保持不变。而且,打破这个jail
是相当容易的描述在这里。
https://deepsec.net/docs/Slides/2015/Chw00t_How_To_Break%20Out_from_Various_Chroot_Solutions_-_Bucsay_Balazs.pdf
pivot_root :https://man7.org/linux/man-pages/man2/pivot_root.2.html
另一方面,Pivot_root
做的正是我们需要的。给定当前根的新根和子目录,它将当前根移动到子目录,并将新根作为根挂载点挂载。通过这种方式,它更改了根目录的物理挂载文件夹。稍后,我们可以卸载“old”根,只留下新创建的根挂载点。我们来看一个例子。
**注:pivot_root
更改了根挂载点,可能会导致文件系统混乱,所以请务必遵循以下步骤。
首先,我们需要一个真正的rootfs
文件系统。我们不能使用上面的例子,因为我们挂载了主机二进制文件。我们需要一个独立的目录,它可以独立存在。为此,我们将使用Docker
从Alpine
容器导出一个新鲜的rootfs
。然后我们将使用unshare
(还记得第0部分中的朋友)来创建一个新的挂载名称空间。然后我们要在容器内以根为中心。它应该是这样的:

将进程监禁在基于apline
的rootfs
中
Docker
导出只是简单地将容器中的文件复制到主机系统的tar
归档文件中。从Alpine
镜像导出rootfs
后,我们绑定挂载目录到它自己,为什么?因为根据pivot_root
系统调用的说明,new_root
必须是与" "不同的挂载点的路径。
在准备容器根目录之后,我们需要创建一个新的挂载命名空间,使其与我们的主机环境不同,这样pivot_root
就不会改变主机挂载命名空间上的任何东西。我们创建一个临时文件夹来保存旧根目录,对根目录进行枢轴操作,卸载旧根目录(或使用umount -l
解除链接),并删除旧根目录来完成交换。瞧!现在我们有一个bash
进程在监禁的容器文件夹内运行。
在Rust
代码中,用nix crate
安装rootfs
文件夹看起来像这样:
nix crate: https://docs.rs/nix/0.22.1/nix/
pub fn mount_rootfs(rootfs: &Path) -> Result<(), Box<dyn Error>> {
mount(
None::<&str>,
"/",
None::<&str>,
MsFlags::MS_PRIVATE | MsFlags::MS_REC,
None::<&str>,
)?;
mount::<Path, Path, str, str>(
Some(&rootfs),
&rootfs,
None::<&str>,
MsFlags::MS_BIND | MsFlags::MS_REC,
None::<&str>,
)?;
Ok(())
}
在 Rust Mount rootfs
第一个挂载将根挂载点的挂载传播更改为私有(由于明显的原因,pivot_root
不允许共享挂载)。整个过程的代码应该是这样的:
pub fn pivot_rootfs(rootfs: &Path) -> Result<(), Box<dyn Error>> {
chdir(rootfs)?;
std::fs::create_dir_all(rootfs.join("oldroot"))?;
pivot_root(rootfs.as_os_str(), rootfs.join("oldroot").as_os_str())?;
umount2("./oldroot", MntFlags::MNT_DETACH)?;
std::fs::remove_dir_all("./oldroot")?;
chdir("/")?;
Ok(())
}
注意,mount_rootfs
和pivot_rootfs
都是在新创建的挂载命名空间中调用的。
特殊链接和安装
OCI
运行时规范定义了一组特殊的符号链接。这些符号链接用于将容器引擎(Docker, containerd
)的stdin、stdout
和stderr
流传递给运行时,反之亦然。它只是将容器的标准流绑定到容器进程的外部文件描述符。容器运行时需要在pivot_root
之前建立这些符号链接。
OCI
运行时规范定义了一组需要挂载到容器中的文件系统。同时提取一些配置。来自apline、Ubuntu、Debian
、/dev/pts
和/dev/shm
等的json
文件都出现在运行时配置规范的挂载部分。
需要更多注意的两个重要文件系统是proc
和sysfs
。
proc
文件系统挂载到/proc
目录,并充当内核内部结构的接口。对于每个进程,它都有一个/proc/[PID]
子目录,用于保存文件描述符、cpu
和内存使用情况、挂载信息、页表和许多其他信息。例如,在没有创建fs
之前,我们不能(使用mount
命令)检查当前的挂载点。挂载proc fs
的确切命令是:
mount -t proc proc /proc
所述的sysfs
文件系统是一个伪FS
状PROC
提供到内部内核对象的接口。与proc
文件系统相反,它保存系统范围的信息,如块和字符设备的元数据、总线信息、驱动程序、控制组、内核信息和其他全局变量。挂载sysfs
与proc
相同:
mount -t sysfs sysfs /sys
既PROC
和sysfs
中需要被安装pivot_root
之后,当新的根挂载创建点。
设备
在 Linux
中,一切都被视为一个文件。硬盘驱动器、外围设备甚至进程都可以通过文件描述符进行完整描述。设备也不例外。软盘、CDROM
、串行端口以及您连接的任何设备都应出现在根目录下的/dev
子目录中。设备有类型,大多数设备是块(存储某种类型的数据)或字符(流或传输数据到/从)设备。终端、伪随机数生成器甚至/dev/null
文件也被视为设备。
OCI
规范定义了每个容器所需的设备,config.json
在linux
部分下包含一个设备列表。容器运行时负责在容器根目录中创建这些设备。创建设备的系统调用是mknod
。此系统调用(也是终端内的命令)接受 4
个必需参数:
路径名 -文件位置的完整路径 type - 块、字符或其他设备类型 主要和次要- 设备的唯一标识符
例如,主要次要编号为 1、8
的字符设备是代表伪随机数生成器的随机设备。每当您的应用请求一个随机数时,此设备都会收到一个请求。
我们可以使用 nix
的mknod
函数轻松生成特殊设备,或者在绑定到主机设备(OCI
规范涵盖)的情况下使用mount bind
选项。
结论
我们已经看到chroot
如何更改当前进程的根目录视图,以及pivot_root
如何交换根挂载点,从而创建文件系统的逻辑隔离。我们还了解了如何创建标准的容器设备,以及不同的容器可以在配置的mount
部分中请求特殊的设备。json
文件。
了解unshare
和pivot_root
是如何工作的,可以让我们在终端中手动创建Linux
容器。在接下来的部分中,我们将更深入地讨论实现。特别是关于克隆子进程和启动容器命令的准备。

5.3 参考资料
参考地址 [1]
参考资料
参考地址: https://penumbra23.medium.com/container-runtime-in-rust-part-i-7bd9a434c50a




