Linux Clone函数

Hive Query生命周期 —— 钩子(Hook)函数篇

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定义,因此直接使用了其值)

newbee-mall 开源商城新计划:秒杀功能、优惠券、对接支付宝

#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

参考

.NET 5网络操作的改进

相关推荐

发表评论

路人甲

网友评论(0)