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
容器。在接下來的部分中,我們將更深入地討論實現。特別是關於克隆子進程和啓動容器命令的準備。
參考資料
[1]
參考地址: https://penumbra23.medium.com/container-runtime-in-rust-part-i-7bd9a434c50a
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/bbGuyB7i4TObEUOifetqbQ