前言
就我目前的對容器的瞭解, 使用namespace
技術實現隔離, 使用cgroups
技術實現資源限制. 但是具體是如何實現卻從未深究過.
閒來無事, 挑其中的Mount Namespace
來康康, 容器是如何實現目錄隔離的.
目錄隔離
在耗子叔的這篇文章中對此技術進行了介紹.
在c
函數庫中, 可通過如下方式實現目錄的隔離:
int container_main(void* arg)
{
// 調用 mount 方法, 觸發目錄隔離機制. 將根目錄替換爲 /root/tmp
mount("/root/tmp", "/", "tmpfs", 0, "");
// dosomething
return 1;
}
void main(){
// 調用 clone 創建子進程
// 傳遞 CLONE_NEWNS 標識, 標明需要創建目錄隔離
clone(container_function, stack, CLONE_NEWNS | SIGCHLD , NULL)
}
如果想在命令行中測試目錄隔離, 也可以如此操作:
# 創建用於掛載的臨時目錄
mkdir -p mount/bin
mkdir -p mount/lib64
mkdir -p mount/lib
# 將執行文件放進去
cp /bin/ls mount/bin
# 將依賴的鏈接庫放入 (依賴庫可通過命令 ldd /bin/ls 查看)
cp /lib/x86_64-linux-gnu/libselinux.so.1 mount/lib
cp /lib/x86_64-linux-gnu/libc.so.6 mount/lib/
cp /lib/x86_64-linux-gnu/libpcre.so.3 mount/lib/
cp /lib/x86_64-linux-gnu/libdl.so.2 mount/lib/
cp /lib64/ld-linux-x86-64.so.2 mount/lib64/
cp /lib/x86_64-linux-gnu/libpthread.so.0 mount/lib/
# 替換運行進程的根目錄
# 執行此命令時, chroot 命令會將 ls 命令的運行根目錄替換爲 ./mount 目錄
# 可以嘗試着執行 /bin/ls 命令查看
chroot ./mount /bin/ls
至此, 雖然舉的例子很簡單, 但依然足夠我們理解目錄隔離了. 容器啓動後, 會將整個進程的根目錄換掉, 甚至直接掛載整個操作系統的ISO. 這也就解釋了爲什麼容器只是一個運行在宿主機上的進程, 卻可以表現爲不同的操作系統.
在docker
中, 容器和鏡像的文件系統目錄, 保存在宿主機的/var/lib/docker/overlay2
.
你可以通過命令docker inspect <container_id>
來查看容器的層級關係.
至於docker
是如何將鏡像的多層進行聚合, 最終展現給容器的, 簡單說是通過UnionFS 技術, 將多個目錄掛載到同一個目錄下, 且可以設置優先級. 因爲使用了union mount
技術, 因此在overlay2
中是看不到容器的完整文件系統的. 它實際上並沒有在磁盤上創建一個包含所有層文件的單一目錄。相反,當你查看容器的文件系統時,Docker 和 Linux 內核會動態地將所有層組合在一起,使它們看起來像一個單一的文件系統。因此,即使你可以在 /var/lib/docker/overlay2
下找到每個層的文件,你也不能直接在這裏找到一個包含容器完整文件系統的單一目錄.
雖然在overlay2
目錄下沒有容器的完整文件系統, 但其實在宿主機的/proc/<pid>/root
目錄中可以看到, 不過前面也說了, 完整的文件系統是動態組合的, 因此/proc/<pid>/root
目錄也只是一個軟連接. 至於容器的pid
, 可以通過命令docker inspect --format '{{ .State.Pid }}' <container_id>
獲取.
exec
目錄隔離使得容器啓動後文件系統自成一派. 那麼執行exec
命令進入容器時, 又是如何做到的呢?
那必然是系統已經支持的啦. 在/proc/<pid>/ns
目錄下, 記錄了命令空間隔離的數據.
其中的mnt
記錄的就是目錄隔離. 可以使用命令行nsenter -t <pid> -m
來進入到特定進程的目錄命名空間. (-m 表示要進入 mount namespace). 命令執行後, 再執行ls
命令, 就會發現已經進入到容器的文件系統中了.
在c
的函數庫中, 則使用setns
函數來實現此功能. 函數具體使用不做贅述.
至此, 容器是如何做到目錄隔離的, 有了一個大致模糊的印象. 收工.