clone(2) - Linux 手册页
名称
clone, __clone2 - 创建子进程
概要
#define _GNU_SOURCE /* See feature_test_macros(7) */ #include <sched.h> int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ... /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );
描述
clone() 以类似于 fork(2) 的方式创建一个新进程。它实际上是一个构建在底层 clone() 系统调用(以下称为 sys_clone)之上的库函数。本页末尾对 sys_clone 进行了描述。
与 fork(2) 不同,这些调用允许子进程与其父进程(调用进程)共享部分执行上下文,例如内存空间、文件描述符表和信号处理程序表。(注意,在本手册页中,“调用进程”通常对应“父进程”。但请参阅下文关于 CLONE_PARENT 的描述。)
clone() 的主要用途是实现线程:程序中在共享内存空间内并发运行的多个控制线程。
当使用 clone() 创建子进程时,它会执行函数 fn(arg)。(这与 fork(2) 不同,后者在子进程中从 fork(2) 调用点继续执行。)fn 参数是一个函数指针,子进程在执行开始时会调用该函数。arg 参数被传递给 fn 函数。
当 fn(arg) 函数返回时,子进程终止。fn 返回的整数即为子进程的退出代码。子进程也可以通过调用 exit(2) 或在接收到致命信号后显式终止。
child_stack 参数指定了子进程所使用栈的位置。由于子进程和调用进程可能共享内存,子进程不可能在与调用进程相同的栈中执行。因此,调用进程必须为子进程栈设置内存空间,并将指向该空间的指针传递给 clone()。在运行 Linux 的所有处理器上(HP PA 处理器除外),栈都是向下增长的,因此 child_stack 通常指向为子进程栈设置的内存空间的最高地址。
flags 的低字节包含了子进程死亡时发送给父进程的终止信号的编号。如果此信号被指定为 SIGCHLD 以外的任何信号,则父进程在调用 wait(2) 等待子进程时必须指定 __WALL 或 __WCLONE 选项。如果不指定信号,则子进程终止时不会向父进程发送信号。
flags 还可以与以下零个或多个常量进行按位或运算,以指定调用进程和子进程之间共享的内容:
- CLONE_CHILD_CLEARTID (自 Linux 2.5.49 起)
- 当子进程退出时,清除子进程内存中 ctid 位置处的子线程 ID,并在该地址处的 futex 上执行唤醒操作。所涉及的地址可以通过 set_tid_address(2) 系统调用进行更改。此功能由线程库使用。
- CLONE_CHILD_SETTID (自 Linux 2.5.49 起)
- 在子进程内存的 ctid 位置处存储子线程 ID。
- CLONE_FILES (自 Linux 2.0 起)
- 如果设置了 CLONE_FILES,则调用进程和子进程共享同一个文件描述符表。由调用进程或子进程创建的任何文件描述符在另一个进程中也有效。同样,如果其中一个进程关闭了文件描述符,或更改了其关联标志(使用 fcntl(2) 的 F_SETFD 操作),另一个进程也会受到影响。
如果未设置 CLONE_FILES,则子进程会继承在 clone() 时调用进程中所有已打开文件描述符的副本。(子进程中复制的文件描述符指向与调用进程中相应文件描述符相同的打开文件描述(参见 open(2))。)之后由调用进程或子进程执行的打开/关闭文件描述符或更改文件描述符标志的操作,都不会影响另一个进程。
- CLONE_FS (自 Linux 2.0 起)
- 如果设置了 CLONE_FS,则调用者和子进程共享相同的文件系统信息。这包括文件系统根目录、当前工作目录和 umask。调用进程或子进程执行的任何 chroot(2)、chdir(2) 或 umask(2) 调用也会影响另一个进程。
如果未设置 CLONE_FS,则子进程在 clone() 调用时使用调用进程文件系统信息的副本。之后任一进程执行的 chroot(2)、chdir(2)、umask(2) 调用都不会影响另一个进程。
- CLONE_IO (自 Linux 2.6.25 起)
- 如果设置了 CLONE_IO,则新进程与调用进程共享一个 I/O 上下文。如果未设置此标志,则(与 fork(2) 一样)新进程拥有自己的 I/O 上下文。
I/O 上下文是磁盘调度程序的 I/O 范围(即 I/O 调度程序用于对进程 I/O 进行建模的依据)。如果进程共享同一个 I/O 上下文,它们会被 I/O 调度程序视为一个整体。因此,它们共享磁盘时间。对于某些 I/O 调度程序,如果两个进程共享一个 I/O 上下文,它们将被允许交错执行磁盘访问。如果多个线程代表同一个进程执行 I/O(例如 aio_read(3)),它们应该使用 CLONE_IO 以获得更好的 I/O 性能。
如果内核未配置 CONFIG_BLOCK 选项,则此标志无效。
- CLONE_NEWIPC (自 Linux 2.6.19 起)
- 如果设置了 CLONE_NEWIPC,则在新的 IPC 命名空间中创建进程。如果未设置此标志,则(与 fork(2) 一样)进程将在与调用进程相同的 IPC 命名空间中创建。此标志旨在用于容器的实现。
IPC 命名空间提供了 System V IPC 对象(参见 svipc(7))和(自 Linux 2.6.30 起)POSIX 消息队列(参见 mq_overview(7))的隔离视图。这些 IPC 机制的共同特征是 IPC 对象通过文件系统路径名以外的机制来标识。
在 IPC 命名空间中创建的对象对该命名空间的所有其他成员进程可见,但对其他 IPC 命名空间中的进程不可见。
当 IPC 命名空间被销毁(即当命名空间中的最后一个成员进程终止)时,该命名空间中的所有 IPC 对象都会自动销毁。
使用此标志需要:内核配置了 CONFIG_SYSVIPC 和 CONFIG_IPC_NS 选项,并且进程具有特权(CAP_SYS_ADMIN)。此标志不能与 CLONE_SYSVSEM 同时指定。
- CLONE_NEWNET (自 Linux 2.6.24 起)
- (此标志的实现直到内核版本 2.6.29 左右才完成。)
如果设置了 CLONE_NEWNET,则在新的网络命名空间中创建进程。如果未设置此标志,则(与 fork(2) 一样)进程将在与调用进程相同的网络命名空间中创建。此标志旨在用于容器的实现。
网络命名空间提供了网络栈的隔离视图(网络设备接口、IPv4 和 IPv6 协议栈、IP 路由表、防火墙规则、/proc/net 和 /sys/class/net 目录树、套接字等)。一个物理网络设备只能存在于一个网络命名空间中。虚拟网络设备(“veth”)对提供了一种类似于管道的抽象,可用于在网络命名空间之间创建隧道,并可用于创建连接到另一个命名空间中物理网络设备的网桥。
当网络命名空间被释放(即命名空间中的最后一个进程终止)时,其物理网络设备会移回初始网络命名空间(而不是移回该进程的父进程)。
使用此标志需要:内核配置了 CONFIG_NET_NS 选项,并且进程具有特权(CAP_SYS_ADMIN)。
- CLONE_NEWNS (自 Linux 2.4.19 起)
- 在新的挂载命名空间中启动子进程。
每个进程都生活在一个挂载命名空间中。进程的命名空间是描述该进程所见文件层次结构的数据(挂载集合)。在未设置 CLONE_NEWNS 标志的 fork(2) 或 clone() 调用之后,子进程生活在与父进程相同的挂载命名空间中。系统调用 mount(2) 和 umount(2) 会更改调用进程的挂载命名空间,从而影响生活在同一命名空间中的所有进程,但不会影响不同挂载命名空间中的进程。
在设置了 CLONE_NEWNS 标志的 clone() 调用之后,克隆出的子进程将在一个新的挂载命名空间中启动,并以父进程命名空间的副本进行初始化。
只有特权进程(拥有 CAP_SYS_ADMIN 能力的进程)才可以指定 CLONE_NEWNS 标志。不允许在同一个 clone() 调用中同时指定 CLONE_NEWNS 和 CLONE_FS。
- CLONE_NEWPID (自 Linux 2.6.24 起)
- 如果设置了 CLONE_NEWPID,则在新的 PID 命名空间中创建进程。如果未设置此标志,则(与 fork(2) 一样)进程将在与调用进程相同的 PID 命名空间中创建。此标志旨在用于容器的实现。
PID 命名空间为 PID 提供了一个隔离环境:新命名空间中的 PID 从 1 开始,有点像一个独立的系统,并且对 fork(2)、vfork(2) 或 clone() 的调用将产生在该命名空间内唯一的 PID 进程。
在新命名空间中创建的第一个进程(即使用 CLONE_NEWPID 标志创建的进程)PID 为 1,是该命名空间的“init”进程。命名空间内的孤儿进程将被重新挂载到此进程,而不是 init(8)。与传统的 init 进程不同,PID 命名空间的“init”进程可以终止;如果它终止了,命名空间内的所有进程也会终止。
PID 命名空间构成层次结构。当创建新的 PID 命名空间时,该命名空间中的进程在创建新命名空间的进程的 PID 命名空间中是可见的;同样地,如果父 PID 命名空间本身又是另一个 PID 命名空间的子进程,那么子和父 PID 命名空间中的进程在祖父 PID 命名空间中都将可见。反之,“子”PID 命名空间中的进程看不到父命名空间中的进程。命名空间层次结构的存在意味着每个进程现在可以有多个 PID:在它可见的每个命名空间中都有一个;这些 PID 在相应的命名空间内都是唯一的。(对 getpid(2) 的调用总是返回与进程所在命名空间相关联的 PID。)
创建新命名空间后,子进程最好更改其根目录并在 /proc 挂载一个新的 procfs 实例,以便诸如 ps(1) 之类的工具能够正常工作。(如果 flags 中也包含了 CLONE_NEWNS,则无需更改根目录:可以直接在 /proc 上挂载新的 procfs 实例。)
使用此标志需要:内核配置了 CONFIG_PID_NS 选项,并且进程具有特权(CAP_SYS_ADMIN)。此标志不能与 CLONE_THREAD 同时指定。
- CLONE_NEWUTS (自 Linux 2.6.19 起)
- 如果设置了 CLONE_NEWUTS,则在新的 UTS 命名空间中创建进程,其标识符通过复制调用进程 UTS 命名空间的标识符进行初始化。如果未设置此标志,则(与 fork(2) 一样)进程将在与调用进程相同的 UTS 命名空间中创建。此标志旨在用于容器的实现。
UTS 命名空间是由 uname(2) 返回的标识符集合;其中,域名和主机名可以分别通过 setdomainname(2) 和 sethostname(2) 进行修改。对 UTS 命名空间中标识符的更改对同一命名空间内的所有其他进程可见,但对其他 UTS 命名空间中的进程不可见。
使用此标志需要:内核配置了 CONFIG_UTS_NS 选项,并且进程具有特权(CAP_SYS_ADMIN)。
- CLONE_PARENT (自 Linux 2.3.12 起)
- 如果设置了 CLONE_PARENT,则新子进程的父进程(由 getppid(2) 返回)将与调用进程的父进程相同。
如果未设置 CLONE_PARENT,则(与 fork(2) 一样)子进程的父进程就是调用进程。
请注意,当子进程终止时,发送信号的对象是父进程(由 getppid(2) 返回),因此如果设置了 CLONE_PARENT,则接收信号的是调用进程的父进程,而不是调用进程本身。
- CLONE_PARENT_SETTID (自 Linux 2.5.49 起)
- 在父进程和子进程内存的 ptid 位置处存储子线程 ID。(在 Linux 2.5.32-2.5.48 中,有一个执行此操作的标志 CLONE_SETTID。)
- CLONE_PID (已弃用)
- 如果设置了 CLONE_PID,子进程将以与调用进程相同的进程 ID 创建。这对于系统黑客操作可能有用,但除此之外用处不大。自 2.3.21 起,此标志只能由系统引导进程(PID 0)指定。它在 Linux 2.5.16 中消失了。
- CLONE_PTRACE (自 Linux 2.2 起)
- 如果指定了 CLONE_PTRACE,并且调用进程正在被跟踪,则同时也跟踪子进程(参见 ptrace(2))。
- CLONE_SETTLS (自 Linux 2.5.32 起)
- newtls 参数是新的 TLS(线程本地存储)描述符。(参见 set_thread_area(2)。)
- CLONE_SIGHAND (自 Linux 2.0 起)
- 如果设置了 CLONE_SIGHAND,则调用进程和子进程共享相同的信号处理程序表。如果调用进程或子进程调用 sigaction(2) 来更改与信号关联的行为,则该行为在另一个进程中也会更改。但是,调用进程和子进程仍然拥有不同的信号掩码和挂起信号集。因此,其中一个进程可以使用 sigprocmask(2) 阻塞或解除阻塞某些信号,而不会影响另一个进程。
如果未设置 CLONE_SIGHAND,则子进程继承 clone() 调用时调用进程的信号处理程序副本。之后任一进程执行的 sigaction(2) 调用对另一个进程没有影响。
自 Linux 2.6.0-test6 起,如果指定了 CLONE_SIGHAND,则 flags 也必须包含 CLONE_VM。
- CLONE_STOPPED (自 Linux 2.6.0-test2 起)
- 如果设置了 CLONE_STOPPED,则子进程最初处于停止状态(就像被发送了 SIGSTOP 信号一样),必须通过向其发送 SIGCONT 信号来恢复。
此标志从 Linux 2.6.25 开始被弃用,并已在 Linux 2.6.38 中被完全移除。
- CLONE_SYSVSEM (自 Linux 2.5.10 起)
- 如果设置了 CLONE_SYSVSEM,则子进程和调用进程共享一个 System V 信号量撤销值列表(参见 semop(2))。如果未设置此标志,则子进程拥有一个单独的、初始为空的撤销列表。
- CLONE_THREAD (自 Linux 2.4.0-test8 起)
- 如果设置了 CLONE_THREAD,则子进程被放置在与调用进程相同的线程组中。为了使后续对 CLONE_THREAD 的讨论更易读,术语“线程”用于指代线程组内的进程。
线程组是 Linux 2.4 中添加的一项功能,旨在支持 POSIX 线程概念,即一组共享单个 PID 的线程。在内部,这个共享的 PID 是线程组的所谓线程组标识符 (TGID)。自 Linux 2.4 起,对 getpid(2) 的调用返回调用者的 TGID。
组内的线程可以通过其(全系统范围内)唯一的线程 ID (TID) 来区分。新线程的 TID 作为函数结果返回给 clone() 的调用者,线程可以使用 gettid(2) 获取自己的 TID。
当在不指定 CLONE_THREAD 的情况下调用 clone() 时,生成的线程被放置在一个新的线程组中,其 TGID 与该线程的 TID 相同。此线程是新线程组的领导者。
使用 CLONE_THREAD 创建的新线程与 clone() 的调用者具有相同的父进程(即类似于 CLONE_PARENT),因此对 getppid(2) 的调用对于线程组中的所有线程返回相同的值。当 CLONE_THREAD 线程终止时,使用 clone() 创建它的线程不会收到 SIGCHLD(或其他终止)信号;也不能使用 wait(2) 获取此类线程的状态。(该线程被称为已分离。)
在线程组中的所有线程都终止后,线程组的父进程会收到 SIGCHLD(或其他终止)信号。
如果线程组中的任何线程执行了 execve(2),则线程组领导者以外的所有线程都会被终止,新程序会在线程组领导者中执行。
如果线程组中的某个线程使用 fork(2) 创建了子进程,则组中的任何线程都可以 wait(2) 该子进程。
自 Linux 2.5.35 起,如果指定了 CLONE_THREAD,则 flags 也必须包含 CLONE_SIGHAND。
可以使用 kill(2) 向整个线程组(即 TGID)发送信号,或使用 tgkill(2) 向特定线程(即 TID)发送信号。
信号处理配置和操作是进程范围的:如果一个未处理的信号被传递给线程,它将影响(终止、停止、继续、被忽略)线程组的所有成员。
每个线程都有自己的信号掩码(由 sigprocmask(2) 设置),但信号可以处于挂起状态:要么是对于整个进程(即可以用 kill(2) 发送并可传递给线程组的任何成员);要么是对于单个线程(用 tgkill(2) 发送)。对 sigpending(2) 的调用返回一个信号集,它是整个进程挂起的信号与调用线程挂起的信号的并集。
如果使用 kill(2) 向线程组发送信号,且线程组已安装了该信号的处理程序,则处理程序将在线程组中随机选择的、未阻塞该信号的恰好一个成员中被调用。如果组中有多个线程正在等待使用 sigwaitinfo(2) 接收同一个信号,内核将随机选择其中一个线程来接收使用 kill(2) 发送的信号。
- CLONE_UNTRACED (自 Linux 2.5.46 起)
- 如果指定了 CLONE_UNTRACED,则跟踪进程无法在该子进程上强制使用 CLONE_PTRACE。
- CLONE_VFORK (自 Linux 2.2 起)
- 如果设置了 CLONE_VFORK,调用进程的执行将被挂起,直到子进程通过调用 execve(2) 或 _exit(2) 释放其虚拟内存资源(与 vfork(2) 一样)。
如果未设置 CLONE_VFORK,则调用进程和子进程在调用后都是可调度的,应用程序不应依赖于任何特定的执行顺序。
- CLONE_VM (自 Linux 2.0 起)
- 如果设置了 CLONE_VM,则调用进程和子进程在同一个内存空间中运行。特别是,调用进程或子进程执行的内存写入操作对另一个进程也可见。此外,子进程或调用进程执行的任何内存映射或取消映射(使用 mmap(2) 或 munmap(2))也会影响另一个进程。
如果未设置 CLONE_VM,子进程将在 clone() 时调用进程内存空间的单独副本中运行。与 fork(2) 一样,一个进程执行的内存写入或文件映射/取消映射操作不会影响另一个进程。
sys_clone
- sys_clone 系统调用更接近于 fork(2),因为它在子进程中的执行从调用点继续。因此,clone() 包装函数的 fn 和 arg 参数被省略。此外,参数顺序也发生了变化。原始系统调用接口大致为:
-
long clone(unsigned long flags, void *child_stack, void *ptid, void *ctid, struct pt_regs *regs); - sys_clone 的另一个区别是 child_stack 参数可以为零,在这种情况下,写时复制 (copy-on-write) 语义确保了当任一进程修改栈时,子进程会获得栈页的单独副本。在这种情况下,为保证正确运行,不应指定 CLONE_VM 选项。
Linux 2.4 及更早版本
- 在 Linux 2.4 及更早版本中,clone() 不接受 ptid、tls 和 ctid 参数。
返回值
成功时,会在调用者的执行线程中返回子进程的线程 ID。失败时,在调用者的上下文中返回 -1,不会创建子进程,并将 errno 设置为相应值。
错误
- EAGAIN
已有太多的进程在运行。
EINVAL
指定了 CLONE_SIGHAND 但未指定 CLONE_VM。(自 Linux 2.6.0-test6 起。)
EINVAL
指定了 CLONE_THREAD 但未指定 CLONE_SIGHAND。(自 Linux 2.5.35 起。)
EINVAL
flags 中同时指定了 CLONE_FS 和 CLONE_NEWNS。
EINVAL
flags 中同时指定了 CLONE_NEWIPC 和 CLONE_SYSVSEM。
EINVAL
flags 中同时指定了 CLONE_NEWPID 和 CLONE_THREAD。
EINVAL
当 child_stack 被指定为 0 时由 clone() 返回。
EINVAL
flags 中指定了 CLONE_NEWIPC,但内核未配置 CONFIG_SYSVIPC 和 CONFIG_IPC_NS 选项。
EINVAL
flags 中指定了 CLONE_NEWNET,但内核未配置 CONFIG_NET_NS 选项。
EINVAL
flags 中指定了 CLONE_NEWPID,但内核未配置 CONFIG_PID_NS 选项。
EINVAL
flags 中指定了 CLONE_NEWUTS,但内核未配置 CONFIG_UTS 选项。
ENOMEM
无法为子进程分配任务结构,或无法复制需要复制的调用者上下文部分。
EPERM
非特权进程(无 CAP_SYS_ADMIN 的进程)指定了 CLONE_NEWIPC、CLONE_NEWNET、CLONE_NEWNS、CLONE_NEWPID 或 CLONE_NEWUTS。
EPERM
除进程 0 之外的进程指定了 CLONE_PID。
版本
libc5 中没有 clone() 的条目。glibc2 提供了本手册页所描述的 clone()。
符合
clone() 和 sys_clone 调用是 Linux 特有的,不应在需要可移植的程序中使用。
说明
在内核 2.4.x 系列中,CLONE_THREAD 通常不会使新线程的父进程与调用进程的父进程相同。但是,对于 2.4.7 到 2.4.18 的内核版本,CLONE_THREAD 标志隐含了 CLONE_PARENT 标志(如同内核 2.6)。
曾经有一段时间存在 CLONE_DETACHED(引入于 2.5.32):父进程不需要子进程退出信号。在 2.6.2 中,不再需要将其与 CLONE_THREAD 一起提供。此标志仍然被定义,但没有任何效果。
在 i386 上,clone() 不应通过 vsyscall 调用,而应直接通过 int $0x80 调用。
在 ia64 上,使用了不同的系统调用。
int __clone2(int (*fn)(void *),
void *child_stack_base, size_t stack_size,
int flags, void *arg, ...
/* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );
__clone2() 系统调用的操作方式与 clone() 相同,只是 child_stack_base 指向子进程栈区域的最低地址,而 stack_size 指定了 child_stack_base 指向的栈大小。错误
包含 NPTL 线程库的 GNU C 库版本包含一个 getpid(2) 的包装函数,该函数会缓存 PID。此缓存依赖于 glibc 中 clone() 包装函数的支持,但按照目前的实现方式,在某些情况下缓存可能无法及时更新。特别地,如果信号在 clone() 调用后立即发送给子进程,那么在信号处理程序中调用 getpid(2) 可能会返回调用进程(即“父进程”)的 PID,前提是 clone 包装函数尚未有机会更新子进程中的 PID 缓存。(此讨论忽略了使用 CLONE_THREAD 创建子进程的情况,在这种情况下,getpid(2) 应该在子进程和调用 clone() 的进程中返回相同的值,因为调用者和子进程处于同一个线程组中。如果 flags 参数包含 CLONE_VM,也不会出现缓存过期的问题。)为了获得真实值,可能需要使用如下代码
#include <syscall.h> pid_t mypid; mypid = syscall(SYS_getpid);
示例
创建一个在单独 UTS 命名空间中执行的子进程
- 以下程序演示了如何使用 clone() 创建在单独 UTS 命名空间中执行的子进程。子进程会更改其 UTS 命名空间中的主机名。父进程和子进程随后都会显示系统主机名,从而可以看到主机名在父进程和子进程的 UTS 命名空间中是不同的。有关此程序的使用示例,请参阅 setns(2)。
#define _GNU_SOURCE #include <sys/wait.h> #include <sys/utsname.h> #include <sched.h> #include <string.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \ } while (0) static int /* Start function for cloned child */ childFunc(void *arg) { struct utsname uts; /* Change hostname in UTS namespace of child */ if (sethostname(arg, strlen(arg)) == -1) errExit("sethostname"); /* Retrieve and display hostname */ if (uname(&uts) == -1) errExit("uname"); printf("uts.nodename in child: %s\n", uts.nodename); /* Keep the namespace open for a while, by sleeping. This allows some experimentation--for example, another process might join the namespace. */ sleep(200); return 0; /* Child terminates now */ } #define STACK_SIZE (1024 * 1024) /* Stack size for cloned child */ int main(int argc, char *argv[]) { char *stack; /* Start of stack buffer */ char *stackTop; /* End of stack buffer */ pid_t pid; struct utsname uts; if (argc < 2) { fprintf(stderr, "Usage: %s <child-hostname>\n", argv[0]); exit(EXIT_SUCCESS); } /* Allocate stack for child */ stack = malloc(STACK_SIZE); if (stack == NULL) errExit("malloc"); stackTop = stack + STACK_SIZE; /* Assume stack grows downward */ /* Create child that has its own UTS namespace; child commences execution in childFunc() */ pid = clone(childFunc, stackTop, CLONE_NEWUTS | SIGCHLD, argv[1]); if (pid == -1) errExit("clone"); printf("clone() returned %ld\n", (long) pid); /* Parent falls through to here */ sleep(1); /* Give child time to change its hostname */ /* Display hostname in parent's UTS namespace. This will be different from hostname in child's UTS namespace. */ if (uname(&uts) == -1) errExit("uname"); printf("uts.nodename in parent: %s\n", uts.nodename); if (waitpid(pid, NULL, 0) == -1) /* Wait for child */ errExit("waitpid"); printf("child has terminated\n"); exit(EXIT_SUCCESS); }
参见
fork(2), futex(2), getpid(2), gettid(2), kcmp(2), set_thread_area(2), set_tid_address(2), setns(2), tkill(2), unshare(2), wait(2), capabilities(7), pthreads(7)