28 进程的创建和执行过程

记账

记账功能打开后,系统会在每个进程结束后记录一条账单信息。标准工具 sa(8) 对账单文件进行汇总,lastcomm(1) 则就先前执行的命令列出相关信息。

Tip

Linux 系统的进程记账功能属于可选内核组件,可以通过 CONFIG_BSD_PROCESS_ACCT 选项进行配置。 在 Debian 上,需要使用 apt install acct 来安装(否则 man 8 sawhich lastcomm 都找不到)。

特权进程可以使用 acct 系统调用来打开或关闭记账功能,应用程序则很少使用到这种调用。

系统调用 clone()

clone() 的地位和 fork()vfork() 一样,它们在内核中都是用 do_fork() 来实现的,但是它能支持比后两个函数更加精细的控制,因此也被用作线程实现的方式。我们一般使用的 clone() 是 glibc 的包装函数,它和内核提供的 SYS_clone 系统调用有一点差异。

/* Prototype for the glibc wrapper function */

#define _GNU_SOURCE
#include <sched.h>

int clone(int (*fn)(void *_Nullable), void *stack, int flags,
         void *_Nullable arg, ...  /* pid_t *_Nullable parent_tid,
                                      void *_Nullable tls,
                                      pid_t *_Nullable child_tid */ );

clone()stack 参数用来指定子进程要使用的栈空间。其实现假设了栈是向下生长的。在 Linux 中,仅有很少见的硬件的栈是向上生长。

clone()flags 参数有两个用途:

  1. flags 的低字节存放了子进程的终止信号,表示子进程终止之后要向父进程发送什么样的信号,在 fork()vfork() 中无法指定子进程终止信号,相当于这个信号只能是 SIGCHLD
  2. 其他字节可以用来存放控制进程创建行为的标志。有不少标志都是内核内部使用的,不是开放给用户程序用的。还有些标志是为了支持线程提供的,有些则是为了支持容器(轻量级虚拟化的一种形式)提供的

NTPL 线程实现占用了 2 个实时信号,可能有个信号就被指定给 clone() 的进程终止信号占用了?

clone() 用于支持线程的标志

通过标志,我们可以指定创建的新进程要和当前进程共享哪些内容,比如:LinuxThreads 线程实现使用了 clone() 的前 4 个参数来创建线程,flags 为 CLONE_VM | CLONE_FILES | CLONE_FS | CLONE_SIGHAND;NPTL 线程实现使用了全部 7 个参数来创建线程,flags 为 CLONE_VM | CLONE_FILES | CLONE_FS | CLONE_SIGHAND | CLONE_THREAD | CLONE_SETTLS | CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID | CLONE_SYSVSEM。这些 flags 的含义分别是(以下进程可能表示一个内核调度实体,也可能表示通常意义上的进程,要注意区分):

  1. CLONE_VM:父子进程共享虚拟内存(这对线程来说显然是必须的)。
  2. CLONE_FILES:共享文件描述符表。这和通常意义上父子进程共享文件描述符(从而共享打开文件的属性、偏移)不同,因为一个线程关闭文件,对另外一个线程来说,文件也就关闭。
  3. CLONE_FS:共享文件系统相关属性。比如当前工作目录、根目录、权限掩码(umask)等。
  4. CLONE_SIGHAND:共享对信号的处理。
  5. CLONE_THREAD:将子进程放在父进程的线程组中。线程组是 Linux 为了实现 POSIX 线程而引入的概念,因为 POSIX 规定同一进程中的所有线程共享一个进程 ID,这个时候线程组 ID 也就成了进程 ID,一个线程组也就是共享线程组标识(TGID)的一组 KSE(内核调度实体)。曾经 Linux 没有 CLONE_THREAD 标志,因此 LinuxThreads 将线程实现为进程号不同、但是共享了许多属性的进程。
  6. CLONE_SETTLS:用来支持线程本地存储。
  7. CLONE_PARENT_SETTID:在 clone() 完成后(还没有返回时就早早地)将子进程的进程号写在父进程地址空间给定的地址(ptid)上。这是为了支持错误处理,因为 clone() 还没有执行完成时可能就会出错,然后触发信号处理器。
  8. CLONE_CHILD_CLEARTID:在子进程退出的时候,将 ctid 指向的位置清零。ctid 指向的位置视同 futex,通过系统调用 futex() 监控 ctid 位置的变化,就可以获得线程终止的通知。
  9. CLONE_SYSVSEM:父子进程共享 System V 信号量撤销 / 还原(英文是 undo)值列表。

除了用 clone() 共享大量属性之外,我们在创建子线程之后还可以用 int unshare(int flags) 来解除属性共享。

Tip

线程组 ID(TGID)就是进程 ID,进程 ID 和线程 ID 都是用 pid_t 表示的。除了线程组主线程的 ID 和线程组 ID 相同之外,线程 ID 在系统范围内唯一。而 pthreads 线程模型在用户空间维护了自己的数据结构,其线程 ID 用 pthread_t 表示。

书中的说法是:

Thread IDs are unique system-wide, and the kernel guarantees that no thread ID will be the same as any process ID on the system, except when a thread is the thread group leader for a process.

其他有意思的标志

  1. CLONE_PARENT:以当前进程的父进程为父进程(即和当前进程为兄弟关系),而不是以当前进程为父进程。Linux 曾用这个标志来支持线程(因为从一个线程创建出的其他线程和当前线程是兄弟关系),直到 CLONE_THREAD 标志的出现使得 Linux 可以使用线程组的概念来创建和管理线程。
  2. CLONE_PID(已经废止):让子进程的 PID 和父进程相同。Linux 2.6 用 CLONE_IDLETASK 来代替它,将新进程的 PID 设置为 0 来为 CPU 的每个核创建隐身的空闲进程(不会被用户看到)。由于 CLONE_IDLETASK 仅供内部使用,所以即便用户指定了它,内核也会忽略这个标志。
  3. CLONE_VFORK:和 vfork() 同语义,父进程将一直挂起直到子进程执行了 _exit() 或者 exec()

不同系统调用创建进程的速度

显而易见,不是很需要记笔记。

exec()fork() 对进程属性的影响

中文书的 505 页。记不住,还是用到了再查吧。

文件描述符的处理方式比较特殊,因为进程间的很多交互都要通过文件描述符进行。fork() 在通常情况下文件描述符会复制一份(因此指向内核打开文件表的相同表项,这使得文件打开属性,如文件偏移量是共享的)、exec() 后文件描述符也不会被清理(除非指定了 close-on-exit)。还有少数通过文件描述符注册的通知机制在 exec() 后仍然保留,但在 fork() 后就失效,比如 dnotify API(目录变更通知)。

和内存相关的一般是 exec() 不复制,而 fork() 复制一份(有可能复制的是类似描述符这样的东西,实际效果是引用)。比如 System V 共享内存段、POSIX 共享内存、POSIX 消息队列、POSIX 命名信号量、备选信号栈(sigaltstack)和信号处理器函数等。

和信号相关的有的是 exec() 保留(比如 setitimer()alarm() 设置的定时器),有的是 fork() 保留,好像看不出来什么规律。挂起信号合集是 exec() 保留、fork() 不保留,信号掩码是两者都保留,exec() 的信号处置方式见 信号处理器和 exec(),而 fork() 保留。