為什麼要用 clone(2)#
unshare(2) 適合「現有行程」自己跳進新的 namespace,而 clone(2) 適合「建立新行程」時就直接讓它出生在新 namespace 中。容器執行環境(runtime)多半使用 clone(2),因為:
- 可以一次傳入多個
CLONE_NEW*flag,原子化建立多種 namespace - 子行程從一開始就是 PID Namespace 中的 PID 1
- 不需要額外 fork
關鍵 Flag#
建立 PID Namespace 對應的 flag:
CLONE_NEWPID:建立新的 PID Namespace- 通常會搭配
CLONE_NEWNS(Mount Namespace)、CLONE_NEWUTS、CLONE_NEWNET等同時使用 - 訊號處理:常用
SIGCHLD作為 child termination signal
C 範例(fork-style)#
以下是一個簡化的範例,示範如何用 clone(2) 建立同時帶有 PID Namespace 與 Mount Namespace 的子行程:
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/mount.h>
#include <signal.h>
#define STACK_SIZE (1024 * 1024)
static int child_fn(void *arg)
{
/* 在新的 PID Namespace 中,這裡的 getpid() 應為 1 */
printf("[child] pid=%d\n", getpid());
/* 重新掛載 /proc,讓 ps 等工具看到正確的 PID 視圖 */
if (mount("proc", "/proc", "proc", 0, NULL) == -1) {
perror("mount /proc");
return 1;
}
char *argv[] = {"/bin/sh", NULL};
execv(argv[0], argv);
perror("execv");
return 1;
}
int main(void)
{
char *stack = malloc(STACK_SIZE);
if (!stack) { perror("malloc"); exit(1); }
int flags = CLONE_NEWPID | CLONE_NEWNS | SIGCHLD;
pid_t pid = clone(child_fn, stack + STACK_SIZE, flags, NULL);
if (pid == -1) { perror("clone"); exit(1); }
printf("[parent] child host pid=%d\n", pid);
waitpid(pid, NULL, 0);
return 0;
}編譯與執行:
gcc -o mini-container mini-container.c
sudo ./mini-container子行程進入後執行 echo $$ 應為 1,ps -ef 應只看到 namespace 內的行程(前提是 /proc 重新掛載成功)。
clone() 與 clone3()#
新的 Kernel 提供 clone3(2),使用 struct clone_args 傳遞參數,更乾淨且支援更多 flag。如果是新撰寫的程式可以考慮使用 clone3(2),但學習階段以 clone(2) 為主已足夠。
與 unshare/setns 的搭配#
實務上的 runtime 往往組合三者:
clone(2)建立主容器行程與大部分 namespaceunshare(2)在某些前置步驟中切換 namespacesetns(2)讓docker exec、nsenter等工具進入容器
自己手寫一個迷你容器是理解 namespaces 最有效的方式,建議從
CLONE_NEWPID+CLONE_NEWNS開始,逐步加入 UTS、NET、USER。
延伸閱讀#
man 2 clone、man 2 clone3- LWN:https://lwn.net/Articles/532593/ ↗ Namespaces in operation, part 1
- Liz Rice, “Containers from Scratch” 演講(YouTube / GOTO Conference)