本文章为对 Litchi Pi
的《Writing a container in Rust》的翻译转载,不享受任何著作权利,不用于任何商业目的,不以任何许可证进行授权,不对任何转载行为尤其是商业转载行为负责。一切权利均由原作者 Litchi Pi 保有。个人翻译能力有限,如有疑问可查看原文。
创建骨架
在这篇文章中,我们将创建容器的骨架,规划这个容器应用的工作流程,并创建一个空的计算程序,随着教程的进展,我们会逐步填充(这个计算程序)所需的部分。
容器基础
现在,项目的基础部分已经可以稳定运行,参数部分也已经收集并验证。接下来我们将把配置提取到一个ContainerOpts
结构体中,并初始化一个执行容器工作的Container
结构体。
配置
让我们在新建的src/config.rs
文件中,为我们容器的配置定义一个结构体
1 | use crate::errors::Errcode; |
让我们来分析一下我们获取的内容:
- path:在容器内执行的二进制文件/可执行文件/脚本的路径。
- argv:传入命令行的全部参数(包括
path
选项)。这些是执行execve 系统调用所必需的,我们将用这个调用来将我们的软件限制在一个执行上下文受限的进程中。
- uid:容器内用户的ID。ID为0意味着它是root用户。
在GNU/Linux上,在
/etc/passwd
文件中可以看到用户ID,该文件的格式为username:x:uuid:guid:comment:homedir:shell
。 - mount_dir:我们在容器内部作为根目录挂载的目录的路径。
随着我们需求的增加,后续会添加更多的配置。
在结构体中我们使用了CString类型,因为它在后续调用execve
系统调用时会更方便。另外,由于我们的配置将与待创建的子进程共享,这个结构体(其中包含堆上存储的数据)需要能够clone
,所以我们需要为结构体添加derive(Clone)
属性。
(请参阅《rust官方文档》中关于所有权(Ownership)与数据复制(Data Copy)章节的内容。)
Cstring是
rust中的一个字符串类型,表示一个拥有的、C兼容的、中间没有nul字节的nul结尾的字符串。
这种类型的作用是能够从Rust字节片或向量安全地生成一个C语言兼容的字符串。这个类型的实例是一个静态的保证,即底层字节不包含内部的0字节(“nul字符”),并且最后的字节是0(“nul结束符”)
关于Cstring
类型的详细信息可以查看文档
接下来让我们创建配置结构体的构造函数。
1 | impl ContainerOpts{ |
这里的操作并不复杂,我们从命令行传入的String
中获取每个参数,并通过将它转换为Vec<CString>
,然后克隆第一个参数,并在返回一个·Ok
结果的同时创建这个结构体。
容器骨架
现在让我们创建用于执行主要任务的Container
结构体
1 | use crate::cli::Args; |
Container
结构体定义了一个唯一的config
字段,其中包含了我们的配置,并实现了三个函数:
new
: 从命令行参数中创建ContainerOpts
结构体。create
: 处理容器创建过程。clean_exit
: 在每次退出前都会被调用,以确保我们执行的操作都会被清理。
现在我们先让它们保持在基础的状态,稍后再来实现他们的功能。
最后,我们需要创建一个start
函数,它会从命令行获取参数,并处理从Container
结构体的创建到退出的所有事情。
它返回一个Result
,用于在程序中发生错误时提供信息。
1 | pub fn start(args: Args) -> Result<(), Errcode> { |
代码中的?
操作符是用来传播错误的。let mut container = Container::new(args)?;
等同于:
1 | let mut container = match Container::new(args) { |
在这种情况下,Err
的类型必须是相同的。
因此如果在我们的项目中为所有错误设定一个唯一的Errcode
会变得很方便,如此一来我们几乎可以在任何地方使用?
操作符,并将所有错误传递到start
函数。start
函数在记录错误并通过我们在错误处理部分定义的exit_with_retcode
函数退出进程时,会先调用 clean_exit
。随后返回一个错误代码。
链接到main函数
最后一步,我们需要从我们的主函数中调用start函数。
在src/main.rs
文件的开始处添加以下代码:
1 | mod container; |
使用exit_with_retcode(container::start(args))
替换exit_with_retcode(Ok(()))
进行测试后我们可以得到如下输出:
1 | [2021-10-02T14:16:41Z INFO crabcan] Args { debug: true, command: "/bin/bash", uid: 0, mount_dir: "./mountdir/" } |
Patch for this step
这一步的代码可以在githublitchipi/crabcan branch “step5”中找到。
从前一步到这一步的原始补丁可以在此处找到。
检查Linux内核版本
这一步完全基于原始教程中的<<check-linux-version>>
部分。
由于我在一个更新版本的内核上进行开发,所以目前我只会检查内核版本是否为v4.8
以上,以及架构是否为x86
。
获取系统信息
由于接下来我们需要开始和操作系统进行交互,并且获取系统信息。所以稍后我们会使用一个非常有用的crate: nix crate
我们先来检查内核版本:
1 | pub const MINIMAL_KERNEL_VERSION: f32 = 4.8; |
在这段代码中,我们首先使用uname获取系统信息。
从获取到的信息中,通过scan_fmt crate将获取内核版本读取为f32
的浮点数(float),并检查它是否至少为v4.8
,然后检查机器架构是否为x86_64
。
错误处理
如果内核版本过低或系统架构不合规,函数将返回Errcode::NotSupported
,它包含了一个表示哪一项不受支持的数字。
如果scan_fmt执行失败,我们将返回Errcode::ContainerError
,这是一个新的错误类型,用于表示在我们的容器中”完全不应该发生”的错误。
让我们把这些新的错误类型追加到src/errors.rs
文件里
1 | pub enum Errcode{ |
添加至流程中并进行测试
我们需要使用的宏来自scan_fmt
包中, 先在src/main.rs
中进行引入:
1 | extern crate scan_fmt; |
在Rust 2015中,在外部预导入包中的crate不能通过use
声明来直接引用,因此通常标准做法是用extern crate
将那它们纳入到当前作用域。从Rust 2018开始,use
声明可以直接引用外部预导入包里的crate,在代码里使用extern crate
会被认为是不规范的。
在Cargo.toml
文件中添加依赖:
1 | [dependancies] |
最后,在src/container.rs
中的start
函数中将check_linux_version
添加到流程中
1 | pub fn start(args: Args) -> Result<(), Errcode> { |
我不打算再在Rust中错误处理的优雅之处这部分内容上多费笔墨,不过你可以看看我们是怎样在不添加错误处理代码的情况下将新函数添加到现有流程中的
在测试后我们可以得到如下结果:
1 | [2021-10-02T14:50:14Z INFO crabcan] Args { debug: true, command: "/bin/bash", uid: 0, mount_dir: "./mountdir/" } |
Patch for this step
这一步的代码可以在github litchipi/crabcan branch “step6”中找到.
前一步到这一步的原始补丁可以在此处找到