Linux Clone函數
之前某一次有過一次面試,問了內核中是怎么創建命名空間的?
下面就來扒一扒clone
的精髓,以及如何通過它創建命名空間。
注:本文的代碼僅用於功能驗證,不能用於生產。本文對clone的標志的描述順序有變,主要考慮到連貫性。
使用clone創建進程和線程
從linux 2.3.3開始,glibc的fork()
封裝作為NPTL(Native POSIX Threads Library)線程實現的一部分。直接調用fork()
等效於調用clone(2)時僅指定flags
為SIGCHLD
(共享信號句柄表)。
創建線程的函數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的使用
下面參照官方幫助文檔一個個解析clone
的flags
的用法。
原型
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)等待子進程退出時必須指定 __WALL 或WCLONE選項。如果沒有指定任何信號(即,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()
的位掩碼為flags
,clone3()
為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_PIDFD
和CLONE_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_NEWNS
和CLONE_FS
(這樣做是相同矛盾的)。
可以使用
nsenter -t <PID> -n
進入網絡命名空間,使用mount
命令查看掛載信息。
CLONE_NEWPID (since Linux 2.6.24)
如果設置了CLONE_NEWPID
,則會在新的PID命名空間中創建進程。如果沒有設置該標志,則新創建的進程與調用進程的PID命名空間相同。
只有特權進程(CAP_SYS_ADMIN
)才可以設置CLONE_NEWPID
。不能在一個clone調用中同時指定CLONE_NEWPID
和CLONE_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_SETUID
和CAP_SETGID
。從Linux 3.8開始,創建用戶命名空間不需要特權。
該標志不能與CLONE_THREAD
或CLONE_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
- 如果要考慮可移植性,盡量使用
fork()
和pthread_create()
- 測試代碼參見:https://github.com/woodliu/linux-clone-test