进程组 -- Process Group
顾名思义就是一组进程. 进程组的 id (pgid) 就是进程组组长(group leader)的 pid. 当一个进程 fork 的时候, 子进程默认是和父进程在同一个进程组的. 从 shell 中启动一个进程的时候, shell 会给这个进程设置为一个新的进程组. 如果使用了 pipe, 那么 shell 会将这些进程放入同一个进程组, 比如 cat hello | less
需要注意的是, 当进程组的 leader 退出的时候, 进程组的其他进程并不会受影响, 系统不会给孤儿进程发送任何信号. 一个进程组在最后一个进程退出时消失.
相关函数
getpgid(pid) - 获得指定 pid 对应的 pgid setpgid(pid, pgid) - 设定指定进程的 pgid
其中可以用 0 来表示当前进程, 如果设置当前进程的 pgid 为自己的 pid, 也就是钦点自己为 group leader, 那么就相当于创建了一个新的进程组.
相关命令
kill 命令用来给 pid 发送信号, 一般命令形式是 kill -SIG PID
, 可以在PID参数前面加上 -
表示一个 Process Group, 而不是 Process. 比如:
kill -TERM -6379 # 向 6379 进程组发送 TERM 信号
回到问题
那么我们现在可以再思考一下刚开始的问题, 为什么按 Ctrl-C 的时候, 父进程和子进程都会收到 SIGINT 信号呢? 答案之前说了:实际上, SIGINT 并不只会发送给前台进程, 而是发送给前台进程组中的每一个进程. 而父进程和子进程当前所在的组正是前台进程组.
前台进程组是一个 session 中在前台运行的那一组进程, 那么什么又是 session 呢?
会话 -- session
session 是一个更大的概念, 一个 session 中可以包含多个 process group.
他们的关系是这样的:
+--------------------------------------------------------------+ | | | pg1 pg2 pg3 pg4 | | +------+ +-------+ +-----+ +------+ | | | bash | | sleep | | cat | | jobs | | | +------+ +-------+ +-----+ +------+ | | session leader | wc | | | +-----+ | | | +--------------------------------------------------------------+ session
和 process group 一样, 每个 session 也有一个 leader, session leader 就是 这个进程的 pid. session 的本意是用来作业控制, 每个用户登录的时候都会创建自己的 session. 一般来说在 shell 中, session leader 就是 shell 本身.
相关函数
getsid(pid) - 获得指定 pid 对应的 sid setsid() - 创建新的session
其中需要注意的是, setsid 不能由 group leader 进程来调用, 因为这样会导致同一个 group 中的进程属于不同的 session, 所以 POSIX 标准直接禁止了这么做.
session 退出
当一个session leader 退出时, 其他进程不会受到任何影响, 但是因为 session leader 退出可能造成 orphaned process group, 因此在shell中, 一般情况下会造成进程退出的情况
Orphaned Process Group
当一个 group leader 退出的时候, 本身并不会对进程组造成任何影响, 也不会收到任何信号. 但是, 当一个进程组变成孤儿进程组(orphaned process group)的时候, 可能会收到一些信号.
孤儿进程组 A process group is called orphaned when the parent of every member is either in the process group or outside the session. In particular, the process group of the session leader is always orphaned.
如果一个进程组中的所有进程的父进程都在组内或者都是其他 session 的进程(比如 init)的时候, 这个进程组被称为孤儿进程组. 显然, 每个进程的退出或者移出进程组都可能造成进程组变成孤儿进程组.
如果这时候进程组中的某个进程的状态是 STOP, 那么内核会向该进程组的所有进程发送 SIGHUP, 并紧接着发送 SIGCONT 信号.
值得注意的是, session leader 本身就是一个孤儿进程组了, 所以退出的时候不会给本组的进程发信号, 下面要用到.
为什么内核要这么做呢?
一般情况下, shell 进程是当前 session 的 leader, 当我们运行每个命令的时候都会创建一个新的 Process Group, 如果这时候某个孤儿进程组中有进程是 STOP 状态的, 那么可能就再也没有机会运行了, 所以系统首先发送 SIGHUP 信号退出, 如果有进程对 SIGINT 做了处理, 那么在收到 SIGCONT 信号之后又可以继续运行了.
也就是说当我们退出 shell 的时候, 内核会向 session 中的
- 前台进程组
- 孤儿进程组
发送 SIGHUP 信号, 从而退出他们. 那么问题来了, 后台进程组呢?
答案是: shell 会向session的所有进程组发送 SIGHUP 信号, 所以运行中的后台进程组也会退出.
daemonize
在 Unix 的上古时期, 没有 Process Manager 这个概念, 所以每个守护进程(比如说 apache)都需要自己变成守护进程, 一般来说是通过 double fork 的形式:
- fork 第一次, 确保自己不是 group leader
- setsid, 创建新的 session
- fork 第二次, 确保自己不是 session leader, 避免获取 tty
实际上整个步骤需要 15 步之多, 可以查看 man 7 daemon
命令.
整个过程非常复杂, 在 GNU C lib 中提供了 daemon() 函数来实现这些步骤, 然而讽刺的是, 由于步骤实在太多了, 系统提供的 daemon 函数竟然忘了其中几步, 所以不推荐使用...
在我看来, 由进程自我守护实际上完全背离的 Unix philosophy -- Write programs that do one thing and do it well, 每个进程应该只做一件事, 变成守护进程显然是让一个进程做了两件事, 而且是一个重复性的工作, 由一个统一的 init 进程来管理 daemon 才是真正符合 Unix 哲学的.
systemd
在现代的 Linux 上, 系统层面, 我们通过 systemd 来管理守护进程, 每个进程只需要实现最简单的单进程程序就好了, 然后通过编写 systemd 的 unit 文件来实现 daemonize. 用户层面, 我们可以使用 supervisord 或者 pm2 来管理进程, 他们和 systemd 的功能和理念都是类似的.
但是, 如上文所述, 一个进程完全可以通过 setsid 和 fork 等操作而完全脱离创建进程的控制, 而且不少进程在创建的时候也是具有 root 权限的, 那么 systemd 是怎样确保进程不会偷偷跑掉的呢?
答案是 cgroups, 且听下回分解...