從零開始寫 Docker(四)---使用 pivotRoot 切換 rootfs 實現檔案系統隔離

探索云原生發表於2024-03-05
change-rootfs-by-pivot-root.png
change-rootfs-by-pivot-root.png

本文為從零開始寫 Docker 系列第四篇,在mydocker run 基礎上使用 pivotRoot 系統呼叫切換 rootfs 實現容器和宿主機之間的檔案系統隔離。


完整程式碼見:https://github.com/lixd/mydocker
歡迎 Star


推薦閱讀以下文章對 docker 基本實現有一個大致認識:

  • 核心原理深入理解 Docker 核心原理:Namespace、Cgroups 和 Rootfs
  • 基於 namespace 的檢視隔離探索 Linux Namespace:Docker 隔離的神奇背後
  • 基於 cgroups 的資源限制
    • 初探 Linux Cgroups:資源控制的奇妙世界
    • 深入剖析 Linux Cgroups 子系統:資源精細管理
    • Docker 與 Linux Cgroups:資源隔離的魔法之旅
  • 基於 overlayfs 的檔案系統Docker 魔法解密:探索 UnionFS 與 OverlayFS
  • 基於 veth pair、bridge、iptables 等等技術的 Docker 網路揭秘 Docker 網路:手動實現 Docker 橋接網路

開發環境如下:

root@mydocker:~# lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 20.04.2 LTS
Release: 20.04
Codename: focal
root@mydocker:~# uname -r
5.4.0-74-generic

注意:需要使用 root 使用者


如果你對雲原生技術充滿好奇,想要深入瞭解更多相關的文章和資訊,歡迎關注微信公眾號。

搜尋公眾號【探索雲原生】即可訂閱


1. 概述

前面幾節中,我們透過 NamespaceCgroups 技術建立了一個簡單的容器,實現了檢視隔離和資源限制。

但是大家應該可以發現,容器內的目錄還是當前執行程式的宿主機目錄,而且如果執行一下 mount 命令可以看到繼承自父程序的所有掛載點。

這貌似和平常使用的容器表現不同

因為這裡缺少了映象這麼一個重要的特性。

Docker 映象可以說是一項偉大的創舉,它使得容器傳遞和遷移更加簡單,那麼這一節會做一個簡單的映象,讓容器跑在有映象的環境中。

即:本章會為我們切換容器的 rootfs,以實現檔案系統的隔離

2. 準備 rootfs

Docker 映象包含了檔案系統,所以可以直接執行,我們這裡就先弄個簡單的,直接將某個映象中的所有內容作為我們的 rootfs 進行掛載。

即:先在宿主機上某一個目錄上準備一個精簡的檔案系統,然後容器執行時掛載這個目錄作為 rootfs

首先使用一個最精簡的映象 busybox 來作為我們的檔案系統。

busybox 是一個集合了非常多 UNIX 工具的箱子,它可以提供非常多在 UNIX 環境下經常使用的命令,可以說 busybox 提供了一個非常完整而且小巧的系統。

因此我們先使用它來作為第一個容器內執行的檔案系統。

獲得 busybox 檔案系統的 rootfs 很簡單,可以使用 docker export 將一個映象打成一個 tar包,並解壓,解壓目錄即可作為檔案系統使用

首先拉取映象

docker pull busybox

然後使用該映象啟動一個容器,並用 export 命令將其匯出成一個 tar 包

# 執行一個互動式命令,讓容器能一直後臺執行
docker run -d busybox top
# 拿到剛建立的容器的 Id
containerId=$(docker ps --filter "ancestor=busybox:latest"|grep -v IMAGE|awk '{print $1}')
echo "containerId" $containerId
# export 從容器匯出
docker export -o busybox.tar $containerId

最後將 tar 包解壓

mkdir busybox
tar -xvf busybox.tar -C busybox/

這樣就得到了 busybox 檔案系統的 rootfs ,可以把這個作為我們的檔案系統使用。

這裡的 rootfs 指解壓得到的 busybox 目錄

busybox 中的內容大概是這樣的:

[root@docker ~]# ls -l busybox
total 16
drwxr-xr-x 2 root root 12288 Dec 29 2021 bin
drwxr-xr-x 4 root root 43 Jan 12 03:17 dev
drwxr-xr-x 3 root root 139 Jan 12 03:17 etc
drwxr-xr-x 2 nfsnobody nfsnobody 6 Dec 29 2021 home
drwxr-xr-x 2 root root 6 Jan 12 03:17 proc
drwx------ 2 root root 6 Dec 29 2021 root
drwxr-xr-x 2 root root 6 Jan 12 03:17 sys
drwxrwxrwt 2 root root 6 Dec 29 2021 tmp
drwxr-xr-x 3 root root 18 Dec 29 2021 usr
drwxr-xr-x 4 root root 30 Dec 29 2021 var

可以看到,內容和一個完整的檔案系統基本是一模一樣的。

注意:rootfs 只是一個作業系統所包含的檔案、配置和目錄,並不包括作業系統核心

在 Linux 作業系統中,這兩部分是分開存放的,作業系統只有在開機啟動時才會載入指定版本的核心映象。

3. 掛載 rootfs

把之前的 busybox rootfs 移動到/root/busybox 目錄下備用。

實現原理

使用pivot_root 系統呼叫來切換整個系統的 rootfs,配合上 /root/busybox 來實現一個類似映象的功能。

pivot_root 是一個系統呼叫,主要功能是去改變當前的 root 檔案系統

原型如下:

#include <unistd.h>

int pivot_root(const char *new_root, const char *put_old);
  • new_root:新的根檔案系統的路徑。
  • put_old:將原根檔案系統移到的目錄。

使用 pivot_root 系統呼叫後,原先的根檔案系統會被移到 put_old 指定的目錄,而新的根檔案系統會變為 new_root 指定的目錄。這樣,當前程序就可以在新的根檔案系統中執行操作。

注意:new_root 和 put_old 不能同時存在當前 root 的同一個檔案系統中。

pivotroot 和 chroot 有什麼區別?

  • pivot_root 是把整個系統切換到一個新的 root 目錄,會移除對之前 root 檔案系統的依賴,這樣你就能夠 umount 原先的 root 檔案系統。

  • 而 chroot 是針對某個程序,系統的其他部分依舊執行於老的 root 目錄中。

具體實現

具體實現如下:

/*
*
Init 掛載點
*/

func setUpMount() {
pwd, err := os.Getwd()
if err != nil {
log.Errorf("Get current location error %v", err)
return
}
log.Infof("Current location is %s", pwd)

// systemd 加入linux之後, mount namespace 就變成 shared by default, 所以你必須顯示
// 宣告你要這個新的mount namespace獨立。
// 如果不先做 private mount,會導致掛載事件外洩,後續執行 pivotRoot 會出現 invalid argument 錯誤
err = syscall.Mount("", "/", "", syscall.MS_PRIVATE|syscall.MS_REC, "")

err = pivotRoot(pwd)
if err != nil {
log.Errorf("pivotRoot failed,detail: %v", err)
return
}

// mount /proc
defaultMountFlags := syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV
_ = syscall.Mount("proc", "/proc", "proc", uintptr(defaultMountFlags), "")
// 由於前面 pivotRoot 切換了 rootfs,因此這裡重新 mount 一下 /dev 目錄
// tmpfs 是基於 件系 使用 RAM、swap 分割槽來儲存。
// 不掛載 /dev,會導致容器內部無法訪問和使用許多裝置,這可能導致系統無法正常工作
syscall.Mount("tmpfs", "/dev", "tmpfs", syscall.MS_NOSUID|syscall.MS_STRICTATIME, "mode=755")
}

func pivotRoot(root string) error {
/**
NOTE:PivotRoot呼叫有限制,newRoot和oldRoot不能在同一個檔案系統下。
因此,為了使當前root的老root和新root不在同一個檔案系統下,這裡把root重新mount了一次。
bind mount是把相同的內容換了一個掛載點的掛載方法
*/

if err := syscall.Mount(root, root, "bind", syscall.MS_BIND|syscall.MS_REC, ""); err != nil {
return errors.Wrap(err, "mount rootfs to itself")
}
// 建立 rootfs/.pivot_root 目錄用於儲存 old_root
pivotDir := filepath.Join(root, ".pivot_root")
if err := os.Mkdir(pivotDir, 0777); err != nil {
return err
}
// 執行pivot_root呼叫,將系統rootfs切換到新的rootfs,
// PivotRoot呼叫會把 old_root掛載到pivotDir,也就是rootfs/.pivot_root,掛載點現在依然可以在mount命令中看到
if err := syscall.PivotRoot(root, pivotDir); err != nil {
return errors.WithMessagef(err, "pivotRoot failed,new_root:%v old_put:%v", root, pivotDir)
}
// 修改當前的工作目錄到根目錄
if err := syscall.Chdir("/"); err != nil {
return errors.WithMessage(err, "chdir to / failed")
}

// 最後再把old_root umount了,即 umount rootfs/.pivot_root
// 由於當前已經是在 rootfs 下了,就不能再用上面的rootfs/.pivot_root這個路徑了,現在直接用/.pivot_root這個路徑即可
pivotDir = filepath.Join("/", ".pivot_root")
if err := syscall.Unmount(pivotDir, syscall.MNT_DETACH); err != nil {
return errors.WithMessage(err, "unmount pivot_root dir")
}
// 刪除臨時資料夾
return os.Remove(pivotDir)
}

然後再 build cmd 的時候指定:

func NewParentProcess(tty bool) (*exec.Cmd, *os.File) {
cmd := exec.Command("/proc/self/exe", "init")
// .. 省略其他程式碼
// 指定 cmd 的工作目錄為我們前面準備好的用於存放busybox rootfs的目錄
cmd.Dir = "/root/busybox"
return cmd, writePipe
}

到此這一小節就完成了,測試一下。

4. 測試

測試比較簡單,只需要執行 ls 命令,即可根據輸出內容確定檔案系統是否切換了。

root@mydocker:~/feat-rootfs/mydocker# go build .
root@mydocker:~/feat-rootfs/mydocker# ./mydocker run -it /bin/ls
{"level":"info","msg":"resConf:\u0026{ 0 }","time":"2024-01-12T16:19:32+08:00"}
{"level":"info","msg":"command all is /bin/ls","time":"2024-01-12T16:19:32+08:00"}
{"level":"info","msg":"init come on","time":"2024-01-12T16:19:32+08:00"}
{"level":"info","msg":"Current location is /root/busybox","time":"2024-01-12T16:19:32+08:00"}
{"level":"info","msg":"Find path /bin/ls","time":"2024-01-12T16:19:32+08:00"}
bin dev etc home proc root sys tmp usr var

可以看到,現在列印出來的就是/root/busybox 目錄下的內容了,說明我們的 rootfs 切換完成。


如果你對雲原生技術充滿好奇,想要深入瞭解更多相關的文章和資訊,歡迎關注微信公眾號。

搜尋公眾號【探索雲原生】即可訂閱


5.小結

本章核心如下:

  • 準備 rootfs:將執行中的 busybox 容器匯出並解壓後作為 rootfs
  • 掛載 rootfs:使用pivotRoot 系統呼叫,將前面準備好的目錄作為容器的 rootfs 使用

在切換 rootfs 之後,容器就實現了和宿主機的檔案系統隔離。

本章使用 pivotRoot 實現檔案系統隔離,加上前面基於 Namespace 實現的檢視隔離,基於 Cgroups 實現的資源限制,至此我們已經實現了一個 Docker 容器的幾大核心功能。


完整程式碼見:https://github.com/lixd/mydocker 歡迎 Star

相關程式碼見 feat-rootfs 分支,測試指令碼如下:

需要提前在 /root/busybox 目錄準備好 rootfs,具體看本文第二節。

# 克隆程式碼
git clone -b feat-rootfs https://github.com/lixd/mydocker.git
cd mydocker
# 拉取依賴並編譯
go mod tidy
go build .
# 測試 檢視檔案系統是否變化
./mydocker run -it /bin/ls

相關文章