為什麼要用 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_NEWUTSCLONE_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 $$ 應為 1ps -ef 應只看到 namespace 內的行程(前提是 /proc 重新掛載成功)。

clone() 與 clone3()#

新的 Kernel 提供 clone3(2),使用 struct clone_args 傳遞參數,更乾淨且支援更多 flag。如果是新撰寫的程式可以考慮使用 clone3(2),但學習階段以 clone(2) 為主已足夠。

與 unshare/setns 的搭配#

實務上的 runtime 往往組合三者:

  • clone(2) 建立主容器行程與大部分 namespace
  • unshare(2) 在某些前置步驟中切換 namespace
  • setns(2)docker execnsenter 等工具進入容器

自己手寫一個迷你容器是理解 namespaces 最有效的方式,建議從 CLONE_NEWPID + CLONE_NEWNS 開始,逐步加入 UTS、NET、USER。

延伸閱讀#