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();

我們將主要關注createstart命令,因爲這是運行docker run命令時最重要的兩個命令。

bundle目錄包含配置。Json文件包含創建容器的所有元數據:

此外,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 目錄中有bashls 。我們來看看 chroot 命令(使用sudo運行):

正如我們所看到的,列出根目錄之外的目錄(ls..)列出了被監禁的根目錄,似乎我們看不到外面的任何東西。此外,列出binlib目錄的結果與上述示例相同。

可以說 “這就是容器被監禁的方式”,然後直接從頭開始構建容器。但是,事情並沒有那麼容易…… 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文件系統。我們不能使用上面的例子,因爲我們掛載了主機二進制文件。我們需要一個獨立的目錄,它可以獨立存在。爲此,我們將使用DockerAlpine容器導出一個新鮮的rootfs。然後我們將使用unshare(還記得第 0 部分中的朋友) 來創建一個新的掛載名稱空間。然後我們要在容器內以根爲中心。它應該是這樣的:

將進程監禁在基於aplinerootfs

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_rootfspivot_rootfs都是在新創建的掛載命名空間中調用的。

特殊鏈接和安裝

OCI運行時規範定義了一組特殊的符號鏈接。這些符號鏈接用於將容器引擎 (Docker, containerd) 的stdin、stdoutstderr流傳遞給運行時,反之亦然。它只是將容器的標準流綁定到容器進程的外部文件描述符。容器運行時需要在pivot_root之前建立這些符號鏈接。

OCI運行時規範定義了一組需要掛載到容器中的文件系統。同時提取一些配置。來自apline、Ubuntu、Debian/dev/pts/dev/shm等的json文件都出現在運行時配置規範的掛載部分。

需要更多注意的兩個重要文件系統是procsysfs

proc文件系統掛載到/proc目錄,並充當內核內部結構的接口。對於每個進程,它都有一個/proc/[PID]子目錄,用於保存文件描述符、cpu和內存使用情況、掛載信息、頁表和許多其他信息。例如,在沒有創建fs之前,我們不能 (使用mount命令) 檢查當前的掛載點。掛載proc fs的確切命令是:

mount -t proc proc /proc

所述的sysfs文件系統是一個僞FSPROC提供到內部內核對象的接口。與proc文件系統相反,它保存系統範圍的信息,如塊和字符設備的元數據、總線信息、驅動程序、控制組、內核信息和其他全局變量。掛載sysfsproc相同:

mount -t sysfs sysfs /sys

PROCsysfs中需要被安裝pivot_root之後,當新的根掛載創建點。

設備

Linux 中,一切都被視爲一個文件。硬盤驅動器、外圍設備甚至進程都可以通過文件描述符進行完整描述。設備也不例外。軟盤、CDROM、串行端口以及您連接的任何設備都應出現在根目錄下的/dev子目錄中。設備有類型,大多數設備是塊(存儲某種類型的數據)或字符(流或傳輸數據到 / 從)設備。終端、僞隨機數生成器甚至/dev/null文件也被視爲設備。

OCI 規範定義了每個容器所需的設備,config.jsonlinux部分下包含一個設備列表。容器運行時負責在容器根目錄中創建這些設備。創建設備的系統調用是mknod。此係統調用(也是終端內的命令)接受 4個必需參數:

例如,主要次要編號爲 1、8的字符設備是代表僞隨機數生成器的隨機設備。每當您的應用請求一個隨機數時,此設備都會收到一個請求。

我們可以使用 nixmknod函數輕鬆生成特殊設備,或者在綁定到主機設備(OCI 規範涵蓋)的情況下使用mount bind選項。

結論

我們已經看到chroot如何更改當前進程的根目錄視圖,以及pivot_root如何交換根掛載點,從而創建文件系統的邏輯隔離。我們還了解了如何創建標準的容器設備,以及不同的容器可以在配置的mount部分中請求特殊的設備。json文件。

瞭解unsharepivot_root是如何工作的,可以讓我們在終端中手動創建Linux容器。在接下來的部分中,我們將更深入地討論實現。特別是關於克隆子進程和啓動容器命令的準備。

參考資料

[1]

參考地址: https://penumbra23.medium.com/container-runtime-in-rust-part-i-7bd9a434c50a

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/bbGuyB7i4TObEUOifetqbQ