Linux Clone函式

charlieroro發表於2021-01-18

Linux Clone函式

之前某一次有過一次面試,問了核心中是怎麼建立名稱空間的?

下面就來扒一扒clone的精髓,以及如何通過它建立名稱空間。

注:本文的程式碼僅用於功能驗證,不能用於生產。本文對clone的標誌的描述順序有變,主要考慮到連貫性

使用clone建立程式和執行緒

從linux 2.3.3開始,glibc的fork()封裝作為NPTL(Native POSIX Threads Library)執行緒實現的一部分。直接呼叫fork()等效於呼叫clone(2)時僅指定flagsSIGCHLD(共享訊號控制程式碼表)。

建立執行緒的函式pthread_create內部使用的也是clone函式。在glibc的/sysdeps/unix/sysv/linux/createthread.c原始碼中可以看到,建立執行緒的函式create_thread中使用了clone函式,並指定了相關的flags

  const int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SYSVSEM
			   | CLONE_SIGHAND | CLONE_THREAD
			   | CLONE_SETTLS | CLONE_PARENT_SETTID
			   | CLONE_CHILD_CLEARTID
			   | 0);

  TLS_DEFINE_INIT_TP (tp, pd);

  if (__glibc_unlikely (ARCH_CLONE (&start_thread, STACK_VARIABLES_ARGS,
				    clone_flags, pd, &pd->tid, tp, &pd->tid)
			== -1))

clone的使用

下面參照官方幫助文件一個個解析cloneflags的用法。

原型

clone提供了兩種呼叫方式,clone3近似可以看作是將clone的入參進行了打包。

 #define _GNU_SOURCE
 #include <sched.h>

int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
    /* pid_t *parent_tid, void *tls, pid_t *child_tid */ );
long clone3(struct clone_args *cl_args, size_t size);

描述

該系統呼叫用於建立一個新的子程式,類似fork(2)。與fork(2)相比,它可以更精確地控制呼叫程式和子程式之間的執行上下文細節。例如,使用這些系統呼叫,呼叫者可以控制兩個程式之間是否共享虛擬地址空間,檔案描述符表以及訊號控制程式碼表等。也可以通過這些系統呼叫將子程式放到不同的名稱空間中。

注:本文中的"呼叫程式"指父程式。

本文描述瞭如下介面:

  • glibc的clone()封裝函式以及依賴的底層系統呼叫。主要描述了封裝函式,原始系統呼叫和封裝函式之間的差異參見文末;
  • 新的clone3()系統呼叫。
clone()封裝函式

當使用clone()建立子程式時,子程式會執行入參的函式fn(與fork(2)不同,fork(2)會從fork函式指定的地方繼續執行)。clone()的入參arg作為函式fn的引數。

fn(arg)函式返回後,子程式就會退出。fn返回的整數為子程式的返回狀態。可以通過呼叫exit(2)或接收終止訊號來結束程式。

stack引數指定了子程式使用的棧的位置。由於子程式和呼叫程式可能會共享記憶體,因此不能在呼叫程式的棧中執行子程式。呼叫程式必須為子程式的棧配置記憶體空間,並向clone()傳入一個執行該空間的指標。執行linux的所有處理器的棧都是向下生長的(HP PA 處理器除外),因此stack通常指向為子程式棧設定的記憶體空間的最頂端地址。注意,clone()沒有為呼叫者提供一種可以將堆疊區域的大小通知核心的方法。

clone()剩下的引數見下。

clone3()

clone3()系統呼叫是老的clone()介面功能的超集。它對API進行了一系列的提升,包括:附加標誌位空間; 運用各種引數進行清理分離; 以及指定子堆疊區域大小的能力。

fork(2), clone3()會同時返回父程式和子程式。而該函式會在子程式中返回0,在父程式中返回子程式的PID。

clone3()cl_args 引數結構如下:

struct clone_args {
    u64 flags;        /* Flags bit mask */
    u64 pidfd;        /* Where to store PID file descriptor
                         (pid_t *) */
    u64 child_tid;    /* Where to store child TID,
                         in child's memory (pid_t *) */
    u64 parent_tid;   /* Where to store child TID,
                         in parent's memory (int *) */
    u64 exit_signal;  /* Signal to deliver to parent on
                         child termination */
    u64 stack;        /* Pointer to lowest byte of stack */
    u64 stack_size;   /* Size of stack */
    u64 tls;          /* Location of new TLS */
    u64 set_tid;      /* Pointer to a pid_t array
                         (since Linux 5.5) */
    u64 set_tid_size; /* Number of elements in set_tid
                         (since Linux 5.5) */
    u64 cgroup;       /* File descriptor for target cgroup
                         of child (since Linux 5.7) */
};

clone3()中的size引數應該初始化為上述結構體的大小(size 引數可以允許未來對clone_args進行擴充套件)。

子程式的棧使用cl_args.stack指定,它指向棧域的最低位元組,cl_args.stack_size指定了棧的位元組大小。當指定CLONE_VM時,必須明確分配並指定棧。否則,這兩個欄位可以指定為NULL和0,這種情況下,子程式會(在其虛擬地址空間中)使用與父程式相同的棧。

cl_args的其他引數見下。

clone() 和clone3()引數的差異

與老的clone()介面不同(老介面的引數是分開傳遞的),新的clone3()介面的引數被打包到了clone_args 結構體中。

下表展示了clone()的引數和clone3()clone_args 結構體欄位的對應關係:

clone()         clone3()        Notes
                cl_args field
flags & ~0xff   flags           For most flags; details below
parent_tid      pidfd           See CLONE_PIDFD
child_tid       child_tid       See CLONE_CHILD_SETTID
parent_tid      parent_tid      See CLONE_PARENT_SETTID
flags & 0xff    exit_signal
stack           stack
---             stack_size
tls             tls             See CLONE_SETTLS
---             set_tid         See below for details
---             set_tid_size
---             cgroup          See CLONE_INTO_CGROUP
子程式結束訊號

當子程式退出時,會像父程式傳送一個訊號。退出訊號在clone()flags的低位元組中指定,或在clone3()中的cl_args.exit_signal欄位指定。如果該訊號不是SIGCHLD,那麼父程式在使用wait(2)等待子程式退出時必須指定 __WALLWCLONE選項。如果沒有指定任何訊號(即,0),則在子程式退出後不會向父程式傳送任何訊號。

set_tid陣列

預設情況下,核心會選擇每個PID名稱空間中的父程式的下一個PID號作為子程式的PID。當使用clone3()建立程式時,可以使用set_tid陣列(linux5.5及以後可用)來為某些或所有PID名稱空間中的程式指定PID。如果僅需要為當前PID名稱空間中或新建立的PID名稱空間中新建立的程式設定程式PID(flags包含CLONE_NEWPID),則set_tid陣列的第一個元素必須為期望的PID,且set_tid_size必須為1(即此時僅有一個程式需要設定PID)。

如果希望給多個PID名稱空間中新建立的程式設定一個特定的PID值,則set_tid 可以包含多個表項。第一個表項定義了最深層巢狀的PID名稱空間中的PID,後續的表項包含在相應的祖先PID名稱空間中的PID。set_tid_size定義了PID名稱空間的數目,且不能大於當前巢狀的PID名稱空間的數目。

如,為了在如下PID名稱空間層次結構中使用如下PIDs建立一個程式:

PID NS level   Requested PID   Notes
0              31496           Outermost PID namespace
1              42
2              7               Innermost PID namespace

設定的set_tid如下:

set_tid[0] = 7;
set_tid[1] = 42;
set_tid[2] = 31496;
set_tid_size = 3;

如果僅需要給最內層的兩個PID名稱空間指定PID,則設定如下:

set_tid[0] = 7;
set_tid[1] = 42;
set_tid_size = 2;

兩個最內層之外的PID名稱空間會使用與其他PID相同的方式選擇PID。

set_tid特性需要在目標PID名稱空間中所擁有的使用者名稱稱空間具有CAP_SYS_ADMIN 或(linux 5.9及之後)CAP_CHECKPOINT_RESTORE 許可權。

如果一個給定的PID名稱空間已經存在init程式,則呼叫者需要選擇一個大於1的PID,否則該PID名稱空間的PID表項必須為1。

flags掩碼

clone()clone3()都執行通過設定flags位掩碼來修改其行為,以及允許呼叫者指定呼叫程式和子程式之間共享的內容。clone()的位掩碼為flagsclone3()cl_args.flags 欄位。

flags掩碼指定為零或以下常量的按位或的結果。除非特殊說明,這些標誌在clone()clone3()中均可用(並具有相同的作用)。

CLONE_CHILD_CLEARTID (since Linux 2.5.49)

當子執行緒存在時,清除(置零)子執行緒記憶體的child_tid(clone()) 或cl_args.child_tid (clone3())上的子執行緒ID,然後在該地址上執行futex。該地址可能被set_tid_address(2) 系統呼叫修改。該標識由執行緒庫使用。

CLONE_CHILD_SETTID (since Linux 2.5.49)

child_tid(clone()) 或cl_args.child_tid (clone3())的位置上儲存執行緒ID。儲存操作會在clone呼叫返回控制到子程式的使用者空間前完成。(注意,在clone呼叫返回父程式前,儲存操作可能是未完成的,它與是否引入CLONE_VM 標誌相關)

CLONE_CLEAR_SIGHAND (since Linux 5.5)

預設情況下,子執行緒中的訊號配置與父執行緒中的相同。如果指定了該標誌,所有父程式處理的訊號在子程式中會被重置為預設配置(SIG_DFL)。

不能將該標誌與CLONE_SIGHAND 共同使用。

CLONE_SIGHAND(since Linux 2.0)

如果設定了CLONE_SIGHAND ,呼叫程式和子程式會共享相同的訊號控制程式碼表。如果呼叫程式或子程式呼叫sigaction(2)修改了某個訊號的行為,那麼此修改也會影響到另一個程式。但此時呼叫程式和子程式仍然具有不同的訊號掩碼和pending的訊號集。為了不影響彼此,可以使用sigprocmask(2)對訊號進行block或unblock。

如果沒有設定CLONE_SIGHAND,則子程式會繼承呼叫程式執行clone期間的一份訊號控制程式碼的拷貝。後續呼叫sigaction(2)將不應影響到另外一個執行緒。

從linux 2.6.0開始,當指定CLONE_SIGHAND後,必須也指定CLONE_VM

測試方式如下,首先指定在建立子程式時指定SIGCHLD

#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

static char child_stack[1048576];

static int child_fn() {
printf("PID: %ld\n", (long)getpid());
sleep (100);
return 0;
}

int main() {
pid_t child_pid = clone(child_fn, child_stack+1048576, CLONE_VM | SIGCHLD, NULL);
printf("clone() = %ld\n", (long)child_pid);

waitpid(child_pid, NULL, 0);
printf("child terminated!\n");
sleep (100);
return 0;
}

編譯並在第一個終端執行該程式:

# ./clone_sighand_test
clone() = 18329
PID: 18329

在當前終端執行"ctrl+c",或在另外一個終端對子程式傳送訊號kill -2 18329,此時可以看到第一個終端輸出如下

# ./clone_sighand_test
clone() = 18329
PID: 18329
child terminated!

當執行clone之後,在父程式中新增對SIGINT訊號的處理,檢視對子程式的影響。

#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>

static char child_stack[1048576];

static int child_fn() {
		printf("PID: %ld\n", (long)getpid());
		sleep (100);
		return 0;
}

static void hdl (int sig, siginfo_t *siginfo, void *context)
{
     printf ("Sending PID: %ld, UID: %ld\n", (long)siginfo->si_pid, (long)siginfo->si_uid);
}

int main() {
     struct sigaction act;

     pid_t child_pid = clone(child_fn, child_stack+1048576, CLONE_VM | CLONE_SIGHAND | SIGCHLD, NULL);
     printf("clone() = %ld\n", (long)child_pid);

     memset (&act, '\0', sizeof(act));

     /* Use the sa_sigaction field because the handles has two additional parameters */
     act.sa_sigaction = &hdl;
     /* The SA_SIGINFO flag tells sigaction() to use the sa_sigaction field, not sa_handler. */
     act.sa_flags = SA_SIGINFO;
     if (sigaction(SIGINT, &act, NULL) < 0) {
				perror ("sigaction");
             return 1;
     }

     waitpid(child_pid, NULL, 0);
     printf("child terminated!\n");
     sleep (100);
     return 0;
}

分別向子程式和父程式傳送SIGINT訊號,可以看到如下輸出。可見在父程式中使用sigaction修改訊號處理的同時也影響到了子程式對該訊號的處理。

# ./clone_sighand_test
clone() = 18728
PID: 18728
Sending PID: 18124, UID: 0
child terminated!
Sending PID: 18124, UID: 0

如果上述程式碼在clone時去掉CLONE_SIGHAND標誌,則執行結果如下,可以看到父程式中對訊號處理的修改並沒有影響到子程式(子程式clone了父程式的一份訊號控制程式碼表,而此時父程式並沒有執行sigaction)。

# ./clone_sighand_test
clone() = PID: 19534
clone() = PID: 19534
19534
child terminated!
Sending PID: 18124, UID: 0

如果要遮蔽特殊的訊號,可以使用sigprocmask遮蔽特定的訊號,防止訊號處理受到其他程式的影響。

static int child_fn() {
     printf("PID: %ld\n", (long)getpid());
     sigset_t new_set;
     sigemptyset( &new_set );
     sigaddset( &new_set, SIGINT );
     sigprocmask(SIG_BLOCK, &new_set, NULL);
     sleep (100);
     return 0;
}

重複上述步驟,可以看到子程式並沒有像父程式一樣處理SIGINT訊號,等待100s之後退出。

# ./clone_sighand_test
clone() = 19659
clone() = 19659
PID: 19659
child terminated!
Sending PID: 18124, UID: 0

下面測試子程式對父程式的影響,僅需要將訊號處理放到子程式即可。

#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>

static char child_stack[1048576];

static void hd (int sig, siginfo_t *siginfo, void *context)
{
     printf ("Sending PID: %ld, UID: %ld\n", (long)siginfo->si_pid, (long)siginfo->si_uid);
}

static int child_fn() {
     printf("PID: %ld\n", (long)getpid());

     struct sigaction act;
     memset (&act, '\0', sizeof(act));

     /* Use the sa_sigaction field because the handles has two additional parameters */
     act.sa_sigaction = &hd;

     /* The SA_SIGINFO flag tells sigaction() to use the sa_sigaction field, not sa_handler. */
     act.sa_flags = SA_SIGINFO;
     if (sigaction(SIGINT, &act, NULL) < 0) {
				perror ("sigaction");
				return 1;
     }
		sleep (100);
		return 0;
}

int main() {
     pid_t child_pid = clone(child_fn, child_stack+1048576, CLONE_VM | CLONE_SIGHAND | SIGCHLD, NULL);
     printf("clone() = %ld\n", (long)child_pid);

     waitpid(child_pid, NULL, 0);
     printf("child terminated!\n");
     sleep (100);
     return 0;
}

重複執行上述操作,可以看到子程式也影響到了父程式對訊號的處理。

CLONE_DETACHED (historical)

在Linux 2.5開發系列中曾有一個CLONE_DETACHED 標誌,當子程式退出之後,會導致父程式無法接收到子程式發來的訊號。在Linux 2.6.2釋出之後,該標誌的功能被合入到了CLONE_THREAD 中,該標記功能廢棄。

現有核心程式碼仍然定義了該標誌,但在呼叫clone()時會被忽略。例外情況參見CLONE_PIDFD

CLONE_PIDFD (since Linux 5.2)

如果指定了該標誌,會分配一個指向子程式的PID檔案描述符,並將其放到父程式指定的記憶體中。新的檔案描述符會設定close-on-exec標誌,其作用參見pidfd_open(2)

  • 當使用clone3(),PID檔案描述符會放到cl_args.pidfd指向的位置。
  • 當使用clone()時,PID檔案描述符會放到parent_tid指向的位置。由於parent_tid 引數用於返回PID檔案描述符,因此當呼叫clone()時,不能同時使用CLONE_PIDFDCLONE_PARENT_SETTID

目前該標誌不能與CLONE_THREAD同時使用,意味著由PID檔案描述符確定的程式總是執行緒組的leader。

如果在呼叫clone()時同時設定了CLONE_PIDFD 和已廢棄的CLONE_DETACHED 標記,則會返回錯誤,類似地,呼叫clone3()時也會返回錯誤。這種行為保證CLONE_DETACHED 對應的位元位可以為將來的PID檔案描述符特性所使用。

CLONE_PARENT_SETTID (since Linux 2.5.49)

在父程式的parent_tid (clone()) 或 cl_args.parent_tid (clone3())中儲存子執行緒ID。在Linux 2.5.32-2.5.48版本中,有一個標誌CLONE_SETTID 做了同樣的事情。儲存操作會在clone呼叫將控制返回給使用者空間前完成。

CLONE_FILES (since Linux 2.0)

如果設定了CLONE_FILES,則呼叫程式和子程式會共享相同的檔案描述符表。呼叫程式或子程式建立的檔案描述符同樣對對方有效。類似地,如果某個程式關閉了檔案描述符,或變更了相關的標誌(使用fcntl(2) F_SETFD操作),同樣會對其他程式生效。如果一個共享檔案描述符表的程式呼叫了 execve(2),則它的檔案描述符表是重複的(非共享)。

如果沒有設定CLONE_FILES ,則在執行clone呼叫時,子程式會繼承呼叫程式的所有開啟的檔案描述符,後續任何一方的開啟、關閉檔案描述符,或修改檔案描述符標誌等操作都不會影響到對方。注意,如果子程式中的檔案描述符與呼叫程式中對應的檔案描述符指向相同的(開啟的)檔案,則會共享相同的檔案偏移和檔案狀態標誌。

在下面程式碼中,在指向clone之後,呼叫程式開啟了一個名為"file.txt"的檔案。

#define _GNU_SOURCE  
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sched.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

#define STACK_SIZE	65536
int fd;

static int child_fn() {
  printf("PID: %ld\n", (long)getpid());
  sleep (100);
}

int main(int argc, char *argv[])
{
		//Allocate stack for child task
		char *stack = malloc(STACK_SIZE);

		if (!stack) {
			perror("Failed to allocate memory\n");
			exit(1);
		}

  pid_t child_pid = clone(child_fn, stack + STACK_SIZE, CLONE_FILES | SIGCHLD, NULL);
  printf("clone() = %ld\n", (long)child_pid);

		fd = open("file.txt", O_RDWR);
		if (fd == -1) {
			perror("Failed to open file\n");
			exit(1);
		}

  waitpid(child_pid, NULL, 0);
  printf("child terminated!\n");
		close(fd);
  sleep (100);

		return 0;
}

使用lsof命令檢視父程式和子程式開啟的檔案,可以看到子程式也開啟了一個file.txt的檔案。由於父程式和子程式開啟的是相同的檔案(無論是否設定了CLONE_FILES),因此當子程式關閉該檔案之後,父程式中對應的檔案也會被關閉。

# lsof -p 20213
COMMAND     PID USER   FD   TYPE DEVICE SIZE/OFF     NODE NAME
...
clone_clo 20213 root    3u   REG  253,0        0  1050946 /root/testclone/file.txt

# lsof -p 20212
COMMAND     PID USER   FD   TYPE DEVICE SIZE/OFF     NODE NAME
...
clone_clo 20212 root    3u   REG  253,0        0  1050946 /root/testclone/file.txt

CLONE_FS (since Linux 2.0)

如果設定了CLONE_FS,則呼叫程式和子程式會共享相同的檔案系統資訊,包括檔案系統的根,當前工作目錄以及umask。任何一方(呼叫程式或子程式)執行了chroot(2), chdir(2), 或 umask(2),都會影響到另一方。

如果沒有設定CLONE_FS,則在執行clone系統呼叫時,子程式會繼承呼叫程式的一份檔案系統資訊的拷貝。此時執行chroot(2), chdir(2), 或 umask(2)不會影響到另一方。

驗證程式碼如下:

#define _GNU_SOURCE
#include <stdio.h>
#include <sched.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

#define STACK_SIZE      65536

static int child_func(void *arg)
{
     printf("Child:Current Working Directory:%s\n",
                     get_current_dir_name());
     chdir("/opt");
     printf("Child:Current Working Directory:%s\n",
                     get_current_dir_name());
     return 0;
}

int main(int argc, char *argv[])
{
     //Allocate stack for child task
     char *stack = malloc(STACK_SIZE);
     int status;
     printf("Parent:Current Working Directory:%s\n",
                     get_current_dir_name());
     if (!stack) {
             perror("Failed to allocate memory\n");
             exit(1);
     }

     if (clone(child_func, stack + STACK_SIZE, CLONE_FS | SIGCHLD, NULL) == -1) {
             perror("clone");
             exit(1);
     }
     if (wait(&status) == -1) {
             perror("Wait");
             exit(1);
     }
     printf("Child exited with status:%d\t cwd:%s\n",
                     status, get_current_dir_name());
     return 0;
}

執行結果如下,可以看到子程式修改的工作路徑影響到了父程式的工作路徑:

# ./clone_clone_fs
Parent:Current Working Directory:/root/linux-clone-test
Child:Current Working Directory:/root/linux-clone-test
Child:Current Working Directory:/opt
Child exited with status:0       cwd:/opt

CLONE_INTO_CGROUP (since Linux 5.7)

需要cgroupv2支援

CLONE_IO (since Linux 2.6.25)

如果設定了CLONE_IO,則新程式會與呼叫程式共享同一個I/O上下文。如果沒有設定該標誌,則新程式會有自己的I/O上下文。

I/O上下文指磁碟排程器的I/O範圍(即I/O排程程式用於對程式的I/O進行排程的模型)。如果程式共享相同的I/O上下文,則I/O排程器會將其視為一個排程單元,結果會導致兩個程式共享磁碟時間。對於某些I/O排程器,如果兩個程式共享一個I/O上下文,將允許這兩個程式交錯訪問磁碟。如果使用多個執行緒代替同一程式執行I/O(例如aio_read(3)),則會獲得更好的I/O效能。如果核心未配置CONFIG_BLOCK選項,則此標誌為無操作。

共享I/O可以提升整體系統的I/O效能,但有可能降低應用本身的I/O。一般I/O比較大的應用會使用多執行緒或多程式方式執行併發I/O操作,達到更好的I/O效能。

CLONE_NEWCGROUP (since Linux 4.6)

在新的cgroup名稱空間中建立程式。如果沒有設定該標誌,則新建立的程式與呼叫程式的cgroup名稱空間相同。

只有特權程式(CAP_SYS_ADMIN)才可以設定CLONE_NEWCGROUP

測試程式碼如下(由於本環境上的sched.h標頭檔案中沒有CLONE_NEWCGROUP定義,因此直接使用了其值)

#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

static char child_stack[1048576];

static int child_fn() {
  printf("PID: %ld\n", (long)getpid());
  sleep(100);
  return 0;
}

int main() {
  pid_t child_pid = clone(child_fn, child_stack+1048576, 0x02000000| SIGCHLD, NULL);
  printf("clone() = %ld\n", (long)child_pid);

  waitpid(child_pid, NULL, 0);
  return 0;
}

執行之後,在/proc/$pid/ns中檢視cgroup的值可以看到其cgroup名稱空間是不同的,同時可以看到其他名稱空間都是相同的。可以在/sys/fs/cgroup下檢視程式的預設cgroup配置,如預設記憶體配置可以檢視/sys/fs/cgroup/memory/user.slice,程式號儲存在/sys/fs/cgroup/memory/user.slice/tasks中。

# ll /proc/20950/ns/
total 0
lrwxrwxrwx. 1 root root 0 Jan  9 20:27 cgroup -> cgroup:[4026532867]
lrwxrwxrwx. 1 root root 0 Jan  9 20:27 ipc -> ipc:[4026531839]
lrwxrwxrwx. 1 root root 0 Jan  9 20:27 mnt -> mnt:[4026531840]
lrwxrwxrwx. 1 root root 0 Jan  9 20:27 net -> net:[4026532000]
lrwxrwxrwx. 1 root root 0 Jan  9 20:27 pid -> pid:[4026531836]
lrwxrwxrwx. 1 root root 0 Jan  9 20:27 pid_for_children -> pid:[4026531836]
lrwxrwxrwx. 1 root root 0 Jan  9 20:27 time -> time:[4026531834]
lrwxrwxrwx. 1 root root 0 Jan  9 20:27 time_for_children -> time:[4026531834]
lrwxrwxrwx. 1 root root 0 Jan  9 20:27 user -> user:[4026531837]
lrwxrwxrwx. 1 root root 0 Jan  9 20:27 uts -> uts:[4026531838]

# ll /proc/20949/ns/
total 0
lrwxrwxrwx. 1 root root 0 Jan  9 20:27 cgroup -> cgroup:[4026531835]
lrwxrwxrwx. 1 root root 0 Jan  9 20:27 ipc -> ipc:[4026531839]
lrwxrwxrwx. 1 root root 0 Jan  9 20:27 mnt -> mnt:[4026531840]
lrwxrwxrwx. 1 root root 0 Jan  9 20:27 net -> net:[4026532000]
lrwxrwxrwx. 1 root root 0 Jan  9 20:27 pid -> pid:[4026531836]
lrwxrwxrwx. 1 root root 0 Jan  9 20:27 pid_for_children -> pid:[4026531836]
lrwxrwxrwx. 1 root root 0 Jan  9 20:27 time -> time:[4026531834]
lrwxrwxrwx. 1 root root 0 Jan  9 20:27 time_for_children -> time:[4026531834]
lrwxrwxrwx. 1 root root 0 Jan  9 20:27 user -> user:[4026531837]
lrwxrwxrwx. 1 root root 0 Jan  9 20:27 uts -> uts:[4026531838]

如果在clone時沒有指定CLONE_NEWCGROUP,則子程式和呼叫程式的cgoup名稱空間是相同的。

CLONE_NEWIPC (since Linux 2.6.19)

如果設定了CLONE_NEWIPC ,則會在新的IPC名稱空間中建立程式。如果沒有設定該標誌,則新建立的程式與呼叫程式的IPC名稱空間相同。

只有特權程式(CAP_SYS_ADMIN)才可以設定CLONE_NEWIPC ,不能與CLONE_SYSVSEM共用(互相矛盾)。

只需修改CLONE_NEWCGROUP中的標誌即可,可以使用nsenter -t <PID> -i進入ipc名稱空間。使用ipcs可以檢視該名稱空間下的ipc資訊。

CLONE_NEWNET (since Linux 2.6.24)

該標誌的實現在核心版本2.6.29中完成。如果設定了CLONE_NEWNET ,則會在新的網路名稱空間中建立程式。如果沒有設定該標誌,則新建立的程式與呼叫程式的網路名稱空間相同。

只有特權程式(CAP_SYS_ADMIN)才可以設定CLONE_NEWNET

可以使用nsenter -t <PID> -i進入網路名稱空間,使用ip a命令檢視網路資訊。

CLONE_NEWNS (since Linux 2.4.19)

如果設定了CLONE_NEWNS ,則會在新的mount名稱空間中建立程式。如果沒有設定該標誌,則新建立的程式與呼叫程式的mount 名稱空間相同。

只有特權程式(CAP_SYS_ADMIN)才可以設定CLONE_NEWNS 。不能在一個clone呼叫中同時指定CLONE_NEWNSCLONE_FS(這樣做是相同矛盾的)。

可以使用nsenter -t <PID> -n進入網路名稱空間,使用mount命令檢視掛載資訊。

CLONE_NEWPID (since Linux 2.6.24)

如果設定了CLONE_NEWPID ,則會在新的PID名稱空間中建立程式。如果沒有設定該標誌,則新建立的程式與呼叫程式的PID名稱空間相同。

只有特權程式(CAP_SYS_ADMIN)才可以設定CLONE_NEWPID。不能在一個clone呼叫中同時指定CLONE_NEWPIDCLONE_THREAD /CLONE_PARENT(CLONE_THREAD和這CLONE_PARENT會修改程式樹,因此是相互矛盾的)。

可以使用nsenter -t <PID> -p進入PID名稱空間,使用ps命令檢視程式資訊。

CLONE_NEWUSER

此標誌最先在Linux 2.6.23中的clone()中啟用,當前的clone()語義已在Linux 3.5中合入,而完整可用的使用者空間功能在Linux 3.8中合入。

如果設定了CLONE_NEWUSER ,則會在新的使用者名稱空間中建立程式。如果沒有設定該標誌,則新建立的程式與呼叫程式的使用者名稱空間相同。

在Linux 3.8之前,使用CLONE_NEWUSER 要求具有3個capability:CAP_SYS_ADMIN, CAP_SETUIDCAP_SETGID。從Linux 3.8開始,建立使用者名稱空間不需要特權。

該標誌不能與CLONE_THREADCLONE_PARENT配合使用。出於安全因素,CLONE_NEWUSER 不能與CLONE_FS配合使用(不同的檔案具有不同的使用者標誌,Linux DAC)。

CLONE_NEWUTS (since Linux 2.6.19)

如果設定了CLONE_NEWUTS ,則會在新的UTS名稱空間中建立程式。如果沒有設定該標誌,則新建立的程式與呼叫程式的UST名稱空間相同。

只有特權程式(CAP_SYS_ADMIN)才可以設定CLONE_NEWUTS

CLONE_PARENT (since Linux 2.3.12)

如果設定了CLONE_PARENT,子程式的父程式(使用getppid(2)獲取)和呼叫程式的父程式相同。

如果沒有設定該標誌,則子程式的父程式就是呼叫程式。

注意,如果設定了CLONE_PARENT ,當子程式退出時,子程式的父程式(而不是呼叫程式)會接收到訊號。

全域性的初始程式(初始PID名稱空間的PID為1的程式)或其他PID名稱空間的初始程式在使用clone時不能設定CLONE_PARENT 標誌。此限制可防止在初始PID名稱空間中建立多root程式樹以及建立不可回收的殭屍程式。

測試程式碼如下:

#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

static char child_stack[1048576];

static int child_fn() {
     printf("child process  parent PID: %ld\n", (long)getppid());
     sleep(100);
     return 0;
}

int main() {
     printf("calling proecess parent PID: %ld\n", (long)getppid());
     pid_t child_pid = clone(child_fn, child_stack+1048576, CLONE_PARENT| SIGCHLD, NULL);
     printf("clone() = %ld\n", (long)child_pid);

     waitpid(child_pid, NULL, 0);
     return 0;
}

執行結果如下:

# ./clone_clone_parent
calling proecess parent PID: 21694
clone() = 23503
child process  parent PID: 21694

**CLONE_PID ** (Linux 2.0 to 2.5.15)

如果設定了CLONE_PID ,則建立的子程式的程式ID會與呼叫程式的程式ID相同。這對於黑客入侵系統很有用,但在其他方面沒有太大用處。從Linux 2.3.21開始,該標誌只能由系統啟動程式(PID 0)進行設定,並在Linux 2.5.16中完全丟棄。如果在flags 掩碼中指定了該標誌,則核心會選擇忽略該標誌,未來將會回收該標誌對應的位元位。

CLONE_PTRACE (since Linux 2.2)

如果指定了該標誌,且正在跟蹤呼叫程式,則子程式也會被跟蹤(參見ptrace(2))

CLONE_UNTRACED (since Linux 2.5.46)

如果指定了該標誌,則不會強制對子程式進行跟蹤。

CLONE_SETTLS (since Linux 2.5.32)

將TLS(Thread Local Storage)儲存到tls欄位中。

tls的解析和相應的影響依賴架構本身。在x86環境上,tls被解析為一個struct user_desc * (參見set_thread_area(2))結構。在86-64環境上,它是為%fs基址暫存器設定的新值(請參見arch_prctl(2)ARCH_SET_FS引數)。在具有專用TLS暫存器的體系結構上,它是該暫存器的新值。

使用此標誌需要詳細的知識體系,通常除非在實現執行緒的庫中使用,否則不應使用此標誌。

CLONE_STOPPED (since Linux 2.6.0)

如果設定了該標誌,則子程式初始是停止的(就像它傳送了一個SIGSTOP訊號一樣),如果要繼續執行,則需要向其傳送一個SIGCONT訊號。

該標誌在Linux 2.6.25之後廢棄,並在Linux 2.6.38中移除,從此之後,Linux會忽略該標誌,從Linux 4.6開始,該標誌對應的位元位被CLONE_NEWCGROUP複用。

CLONE_SYSVSEM (since Linux 2.5.10)

如果設定了該標誌,則子程式和呼叫程式會共享一組System V semaphore adjustment (semadj) 值(參見semop(2))。這種情況下,共享列表會在共享該列表的所有程式之間累加semadj值,並且僅當共享列表的最後一個程式終止(或使用unshare(2)停止共享列表)時才會執行semaphore adjustments。如果沒有設定該標誌,則子程式會有一個獨立的semadj 列表,且初始為空。

與訊號量操作有關。

CLONE_THREAD (since Linux 2.4.0)

如果設定了該標誌,則子執行緒會放到與呼叫程式相同的執行緒組中。為了防止概念混淆,術語"執行緒"指代一個執行緒組中的程式。

執行緒組是Linux 2.4中新增的一項功能,用於支援一組POSIX執行緒共享一個PID。在內部,該共享的PID是執行緒組的執行緒組識別符號(TGID)。從Linux 2.4開始,getpid(2)會返回撥用者的TGID。

組中的執行緒可以通過其(系統範圍內的)唯一執行緒ID(TID)進行區分。新執行緒的TID可用作返回給呼叫方的結果,執行緒可以使用gettid(2)獲得自己的TID。

當一個clone呼叫沒有指定CLONE_THREAD時,生成的執行緒會放到一個新的執行緒組中,其TGID等於該執行緒的TID,該執行緒為新執行緒組的leader。

使用CLONE_THREAD 建立出來的新執行緒具有與呼叫執行緒系統的父程式(與CLONE_PARENT類似),因此在該執行緒中呼叫getppid(2) 會返回與一個執行緒組中的所有執行緒相同的結果。當一個CLONE_THREAD 的執行緒結束後,建立的執行緒不會傳送SIGCHLD(或其他結束)訊號,因此無法使用wait(2)獲取這類執行緒的狀態(可以認為該程式被detached)。

執行緒組中的所有執行緒終止後,會向該執行緒組的父程式傳送SIGCHLD(或其他終止)訊號。

如果執行緒組中的任一執行緒執行了execve(2),則終止除執行緒組leader之外的所有執行緒,並線上程組leader中執行新程式。

如果執行緒組中的任一執行緒使用fork(2)建立了子程式,則組中的任意執行緒都可以使用wait(2)獲取該子程式的狀態。

從Linux 2.5.35開始,如果指定了CLONE_THREAD,則必須同時指定CLONE_SIGHAND (注意,從Linux 2.6.0開始,指定CLONE_SIGHAND 的同時也必須指定CLONE_VM)。

訊號的處理和動作是程式級別的:如果一個未處理的訊號傳遞到了一個執行緒,那麼該訊號會影響(終止,停止,繼續或忽略)到執行緒組中的所有成員。

每個執行緒都有自己的訊號掩碼,可以使用 sigprocmask(2)設定。

訊號可以是程式控制或執行緒控制的。一個程式控制的訊號會發往一個執行緒組(即TGID),然後該訊號會傳遞到沒有阻塞該訊號的任一個執行緒中。如果一個訊號是由核心出於硬體異常以外的原因生成,或通過kill(2)sigqueue(3)傳送的,則它是程式控制的;執行緒控制的訊號會發往一個特定的執行緒。如果一個訊號是使用tgkill(2)pthread_sigqueue(3)傳送的,或者因為該執行緒執行了觸發硬體異常的機器語言指令(例如,無效的記憶體訪問觸發了SIGSEGV或浮點異常觸發了 SIGFPE),則該訊號是執行緒控制的。

sigpending(2)的呼叫會返回一個訊號集,該訊號集是pending的程式控制訊號和呼叫執行緒的pending訊號的並集。

如果一個程式控制的訊號傳遞給了一個執行緒組,且執行緒組為該訊號安裝了一個處理器,則會在任意一個沒有阻塞該訊號的執行緒中呼叫該處理器。如果一個組中的多個執行緒通過sigwaitinfo(2)等待接收相同的訊號,則核心會任意選擇其中之一來接收該訊號。

CLONE_VFORK (since Linux 2.2)

如果設定了該標誌,則呼叫程式的執行會被掛起,直到子程式通過execve(2)_exit(2) (類似vfork(2))釋放了其虛擬記憶體資源。

如果沒有設定該標誌,則呼叫程式和子程式在執行clone之後都可以被正常排程,且應用不需要依賴特定的執行順序。

測試程式碼如下:

#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

static char child_stack[1048576];

static int child_fn() {
     printf("child process  parent PID: %ld\n", (long)getpid());
     sleep(100);
     return 0;
}

int main() {
     printf("calling proecess parent PID: %ld\n", (long)getpid());
     pid_t child_pid = clone(child_fn, child_stack+1048576, CLONE_VFORK | SIGCHLD, NULL);
     printf("clone() = %ld\n", (long)child_pid);

     waitpid(child_pid, NULL, 0);
     return 0;
}

執行結果如下,可以看到在子程式退出之前,父程式是不會繼續執行的:

# ./clone_clone_vfork
calling proecess PID: 25319
child process PID: 25320  #這一步會等待10s
clone() = 25320

另外一個是呼叫execve,只要子程式呼叫了execve,父程式就可以繼續執行,無需等待子程式的結束。

static int child_fn() {
     printf("child process  parent PID: %ld\n", (long)getpid());
     char *argv[ ]={"ls", "-al", "/etc/passwd", NULL};
     char *envp[ ]={"PATH=/bin", NULL};
     execve("/bin/ls", argv, envp);
     sleep(100);
     return 0;
}

執行結果如下:

# ./clone_clone_vfork
calling proecess PID: 25420
child process  parent PID: 25421
clone() = 25421  #這一步會等待10s

:fork是分身,execve是變身。

exec系列的系統呼叫是把當前程式替換成要執行的程式,而fork用來產生一個和當前程式一樣的程式。通常執行另一個程式,而同時保留原程式執行的方法是,fork+exec。

CLONE_VM (since Linux 2.0)

如果設定了CLONE_VM ,則呼叫程式和子程式會執行在系統的記憶體空間中。呼叫程式或子程式對記憶體的寫操作都可以被對方看到。此外使用mmap(2)munmap(2)執行的對映或去對映也會影響到另外一個程式。

如果沒有設定CLONE_VM ,則子程式會執行在執行clone時的呼叫程式的一份記憶體空間的拷貝中。此時對記憶體的寫入或檔案的mappings/unmappings都不會影響到對方(fork(2)就是這麼做的)。

測試程式碼如下:

#define _GNU_SOURCE
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sched.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <errno.h>
#include <sys/stat.h>

#define STACK_SIZE      65536
int fd;

static int child_fn() {
     printf("PID: %ld\n", (long)getpid());
     sleep (100);
}

int main(int argc, char *argv[])
{
     int fd = 0;
     char *ptr = NULL;
     struct stat buf = {0};

     //Allocate stack for child task
     char *stack = malloc(STACK_SIZE);

     if (!stack) {
             perror("Failed to allocate memory\n");
             exit(1);
     }

     pid_t child_pid = clone(child_fn, stack + STACK_SIZE, CLONE_VM |SIGCHLD, NULL);
     printf("clone() = %ld\n", (long)child_pid);

     if ((fd = open("file.txt", O_RDWR)) < 0)
     {
             printf("open file error\n");
             return -1;
     }

     if (fstat(fd, &buf) < 0)
     {
             printf("get file state error:%d\n", errno);
             close(fd);
             return -1;
     }

     ptr = (char *)mmap(NULL, buf.st_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
     if (ptr == MAP_FAILED)
     {
             printf("mmap failed\n");
             close(fd);
             return -1;
     }

     waitpid(child_pid, NULL, 0);
     munmap(ptr, buf.st_size);
     close(fd);
     sleep (100);
     return 0;
}

執行上述命令之後,在另一個終端執行如下命令,可以發現,呼叫程式和子程式中都可以看到該對映的檔案。如果不帶該標誌,則只有呼叫程式可以看到該對映的檔案。

# lsof -ad mem file.txt
COMMAND     PID USER  FD   TYPE DEVICE SIZE/OFF    NODE NAME
clone_clo 25619 root mem    REG  253,0     8001 1050950 file.txt
clone_clo 25620 root mem    REG  253,0     8001 1050950 file.txt

注:CLONE_FILES 共享的是檔案描述符表,而共享的是記憶體。

備註

這些系統呼叫的一個用處是實現執行緒:一個程式中,在一個共享的地址空間中併發的多條控制流。

Glibc沒有提供clone3()的封裝,使用syscall(2)進行呼叫。

注意,在呼叫clone()系統呼叫之前,glibc clone()封裝函式會對堆疊指向的記憶體進行一些更改(為子程式正確設定堆疊所需的更改)。因此,在使用clone()遞迴建立子程式的情況下,不能將父程式棧的緩衝區用於子程式棧。

kcmp(2)系統呼叫可以用於測試兩個程式是否共享相同的資源,如檔案描述符表,System V 訊號量未執行的操作,或虛擬地址空間。

在clone呼叫期間不會執行使用pthread_atfork(3)註冊的處理器。

在Linux 2.4.x系列中,CLONE_THREAD 通常不會將新執行緒的父程式設定為呼叫程式的父程式。但在2.4.7 到2.4.18核心版本時,CLONE_THREAD 暗含了CLONE_PARENT 標誌(Linux 2.6.0及之後)。

TIPs

參考

相關文章