容器核心技術--Namespace
上文提到,容器技術的核心有兩個:Namespace 和 Cgroup。本節先來介紹 Namespace 是什麼。
簡單來說,Namespace 可以為容器提供系統資源隔離能力。
當然,這樣講過於籠統,我們來舉個例子:假如一個容器中的程序需要使用 root 許可權,出於安全考慮,我們不可能把宿主機的 root 許可權給他。但是通過 Namespace 機制,我們可以隔離宿主機與容器的真實使用者資源,謊稱一個普通使用者就是 root,騙過這個程式。從這個角度看,Namespace 就是核心對程序說謊的機制,目前(Linux最新的穩定版本為5.6),核心可以說的謊話有 8 種:
Namespace | 系統呼叫 |
---|---|
Mount | CLONE_NEWNS |
UTS | CLONE_NEWUTS |
IPC | CLONE_NEWIPC |
PID | CLONE_NEWPID |
Network | CLONE_NEWNET |
User | CLONE_NEWUSER |
Cgroup | CLONE_NEWCGROUP |
Time | CLONE_NEWTIME |
1. Namespace 詳解
1.1 Mount Namespace
Mount Namespace 用來隔離檔案系統的掛載點,不同的 Mount namespace 擁有各自獨立的掛載點資訊。在 Docker 這樣的容器引擎中,Mount namespace 的作用就是保證容器中看到的檔案系統的檢視。
1.2 UTS Namespace
UTS Namespace 用來隔離系統的主機名、hostname 和 NIS 域名。
1.3 IPC Namespace
IPC 就是在不同程序間傳遞和交換資訊。IPC Namespace 使得容器內的所有程序,進行的資料傳輸、共享資料、通知、資源共享等範圍控制在所屬容器內部,對宿主機和其他容器沒有干擾。
1.4 PID Namespace
PID namespaces用來隔離程序的 ID 空間,使得不同容器裡的程序 ID 可以重複,相互不影響。
1.5 Network Namespace
Network namespace 用來隔離網路,每個 namespace 可以有自己獨立的網路棧,路由表,防火牆規則等
1.6 user namespace
user namespace 是例子中講到的,控制使用者 UID 和 GID 在容器內部和宿主機上的一個對映,主要用來管理許可權。
1.7 Time namespace
這個 Namespace 允許作業系統為程序設定不同的系統時間。
1.8 Cgroup Namespace
這個 Namespace 用來限制 CGroup 根目錄下不同層級目錄的許可權,使得 CGROUP 根目錄下的子目錄的程序無法影響到父目錄。
2. 實踐出真知
2.1 感受 Namespace
講了那麼多理論,Namespace 能不能讓我們直接觀察到呢?
讓我們進入 Linux 環境,執行如下操作:
# 進入/proc/目錄
cd /proc/
# 檢視當前目錄下有哪些檔案或目錄
ls
# 隨便進入一個以數字(程序號)命名的目錄,比如1
cd 1
# 檢視ns(Namespace)目錄下的內容
ls -al ns
當前目錄下紅色的連結,就是這個程序對應的 Namespace。
有興趣的讀者可以看看其他不同程序的 Namespace,比對下是否有差異。如果你找到某個程序的Namespace 與其他的不一致,就說明這個程序指定了 Namespace 隔離。
2.2 使用 Namespace 自制簡易容器
將以下程式碼儲存到/root/test/container.c
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mount.h>
#include <sys/capability.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>
#define STACK_SIZE (1024 * 1024)
static char container_stack[STACK_SIZE];
char* const container_args[] = {
"/bin/bash",
NULL
};
int pipefd[2];
void set_map(char* file, int inside_id, int outside_id, int len) {
FILE* mapfd = fopen(file, "w");
if (NULL == mapfd) {
perror("open file error");
return;
}
fprintf(mapfd, "%d %d %d", inside_id, outside_id, len);
fclose(mapfd);
}
void set_uid_map(pid_t pid, int inside_id, int outside_id, int len) {
char file[256];
sprintf(file, "/proc/%d/uid_map", pid);
set_map(file, inside_id, outside_id, len);
}
void set_gid_map(pid_t pid, int inside_id, int outside_id, int len) {
char file[256];
sprintf(file, "/proc/%d/gid_map", pid);
set_map(file, inside_id, outside_id, len);
}
int container_main()
{
char ch;
close(pipefd[1]);
read(pipefd[0], &ch, 1);
sethostname("container",10);
/* Mount Namespace */
mount("proc", "/proc", "proc", 0, NULL);
mount("none", "/tmp", "tmpfs", 0, "");
execv(container_args[0], container_args);
return 1;
}
int main()
{
const int gid=getgid(), uid=getuid();
pipe(pipefd);
int container_pid = clone(container_main, container_stack+STACK_SIZE,
CLONE_NEWCGROUP|CLONE_NEWIPC|CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWUSER | SIGCHLD, NULL);
set_uid_map(container_pid, 0, uid, 1);
set_gid_map(container_pid, 0, gid, 1);
close(pipefd[1]);
waitpid(container_pid, NULL, 0);
return 0;
}
我們不用讀懂這個程式碼,只需要留意下 main 主函式中這部分
int container_pid = clone(container_main, container_stack+STACK_SIZE,
CLONE_NEWCGROUP|CLONE_NEWIPC|CLONE_NEWNET|CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWUSER | SIGCHLD, NULL);
這段程式碼 呼叫 clone
實現執行緒的系統呼叫,用來建立一個新的程序,並可以通過設計上述引數達到隔離。
執行下面的操作
# 安裝可能需要的依賴
sudo dnf install -y libcap-devel
# 編譯這個檔案
cc container.c -o container
# 執行
./container
執行我們編譯好的container程式後,發現我們處於一個新的環境的終端中,你可以在這裡驗證你的猜測,比如檢視當前環境的程序 ps
,當前登入的使用者 whoami
,網路狀況 ip a
等等,使用exit
可以退出回到原來的環境。
我們確實通過系統呼叫,建立了一個與宿主機資源隔離的容器環境。
3. 小結
本節我們介紹了 Namespace 機制,和它的 8 種隔離型別,並實現了一具有名稱空間隔離功能的“容器”,在這個過程中,希望大家對容器和 Namespace 機制有了更深入的理解。