28 进程的创建和执行过程
记账
记账功能打开后,系统会在每个进程结束后记录一条账单信息。标准工具 sa(8) 对账单文件进行汇总,lastcomm(1) 则就先前执行的命令列出相关信息。
Tip
Linux 系统的进程记账功能属于可选内核组件,可以通过 CONFIG_BSD_PROCESS_ACCT
选项进行配置。 在 Debian 上,需要使用 apt install acct
来安装(否则 man 8 sa
和 which 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
参数有两个用途:
flags
的低字节存放了子进程的终止信号,表示子进程终止之后要向父进程发送什么样的信号,在fork()
和vfork()
中无法指定子进程终止信号,相当于这个信号只能是SIGCHLD
。- 其他字节可以用来存放控制进程创建行为的标志。有不少标志都是内核内部使用的,不是开放给用户程序用的。还有些标志是为了支持线程提供的,有些则是为了支持容器(轻量级虚拟化的一种形式)提供的。
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 的含义分别是(以下进程可能表示一个内核调度实体,也可能表示通常意义上的进程,要注意区分):
CLONE_VM
:父子进程共享虚拟内存(这对线程来说显然是必须的)。CLONE_FILES
:共享文件描述符表。这和通常意义上父子进程共享文件描述符(从而共享打开文件的属性、偏移)不同,因为一个线程关闭文件,对另外一个线程来说,文件也就关闭。CLONE_FS
:共享文件系统相关属性。比如当前工作目录、根目录、权限掩码(umask)等。CLONE_SIGHAND
:共享对信号的处理。CLONE_THREAD
:将子进程放在父进程的线程组中。线程组是 Linux 为了实现 POSIX 线程而引入的概念,因为 POSIX 规定同一进程中的所有线程共享一个进程 ID,这个时候线程组 ID 也就成了进程 ID,一个线程组也就是共享线程组标识(TGID)的一组 KSE(内核调度实体)。曾经 Linux 没有CLONE_THREAD
标志,因此 LinuxThreads 将线程实现为进程号不同、但是共享了许多属性的进程。CLONE_SETTLS
:用来支持线程本地存储。CLONE_PARENT_SETTID
:在 clone() 完成后(还没有返回时就早早地)将子进程的进程号写在父进程地址空间给定的地址(ptid
)上。这是为了支持错误处理,因为clone()
还没有执行完成时可能就会出错,然后触发信号处理器。CLONE_CHILD_CLEARTID
:在子进程退出的时候,将ctid
指向的位置清零。ctid
指向的位置视同 futex,通过系统调用futex()
监控ctid
位置的变化,就可以获得线程终止的通知。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.
其他有意思的标志
CLONE_PARENT
:以当前进程的父进程为父进程(即和当前进程为兄弟关系),而不是以当前进程为父进程。Linux 曾用这个标志来支持线程(因为从一个线程创建出的其他线程和当前线程是兄弟关系),直到CLONE_THREAD
标志的出现使得 Linux 可以使用线程组的概念来创建和管理线程。CLONE_PID
(已经废止):让子进程的 PID 和父进程相同。Linux 2.6 用CLONE_IDLETASK
来代替它,将新进程的 PID 设置为 0 来为 CPU 的每个核创建隐身的空闲进程(不会被用户看到)。由于CLONE_IDLETASK
仅供内部使用,所以即便用户指定了它,内核也会忽略这个标志。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()
保留。