第二十八章:进程家族
筑基中期涉及内核源码:
一
林小源在观察 shell 小妹的过程中,发现了一个他之前忽略的结构。
进程不是孤立的。它们有"家族"——进程组和会话。
shell 小妹执行一条命令时,fork 出来的子进程和 shell 属于同一个进程组。进程组有一个组长(通常是 shell),组长的 PID 就是进程组 ID(PGID)。
/*
* 进程的组织结构:
* 会话 (Session)
* └── 前台进程组 (Foreground Process Group)
* └── 进程 (Process)
* └── 后台进程组 (Background Process Group)
* └── 进程 (Process)
*
* 关键系统调用:
* setpgid(pid, pgid) — 设置进程组
* setsid() — 创建新会话
* tcsetpgrp(fd, pgid) — 设置前台进程组
*/
struct process {
int pid;
int pgid; /* 进程组 ID */
int sid; /* 会话 ID */
char comm[16];
};
struct session {
int sid;
int fg_pgid; /* 前台进程组 */
struct process *leader;
};
struct process_group {
int pgid;
struct process *procs[8];
int nr_procs;
};
printf("=== 进程组与会话 ===\n\n");
/* shell 小妹的会话 */
struct process shell = { .pid = 100, .pgid = 100, .sid = 100, .comm = "bash" };
struct process grep = { .pid = 201, .pgid = 200, .sid = 100, .comm = "grep" };
struct process sort = { .pid = 202, .pgid = 200, .sid = 100, .comm = "sort" };
struct process vim = { .pid = 300, .pgid = 300, .sid = 100, .comm = "vim" };
struct session sess = {
.sid = 100,
.fg_pgid = 200, /* grep | sort 是前台 */
.leader = &shell,
};
printf("会话 SID=100 (leader: %s)\n", shell.comm);
printf(" 前台进程组 PGID=200:\n");
printf(" PID %d (%s)\n", grep.pid, grep.comm);
printf(" PID %d (%s)\n", sort.pid, sort.comm);
printf(" 后台进程组 PGID=300:\n");
printf(" PID %d (%s)\n", vim.pid, vim.comm);
printf("\n--- Ctrl+C 的旅程 ---\n");
printf("1. 用户按下 Ctrl+C\n");
printf("2. 终端驱动发送 SIGINT\n");
printf("3. SIGINT 发送给前台进程组的所有进程\n");
printf(" 前台进程组 PGID=%d\n", sess.fg_pgid);
printf(" → PID %d (%s) 收到 SIGINT\n", grep.pid, grep.comm);
printf(" → PID %d (%s) 收到 SIGINT\n", sort.pid, sort.comm);
printf(" → PID %d (%s) 不受影响(后台)\n", vim.pid, vim.comm);
printf("\n--- fg/bg 命令 ---\n");
printf("fg %%vim: 把 vim 移到前台\n");
printf(" tcsetpgrp(STDIN, 300)\n");
printf(" 前台进程组变为 PGID=300\n");
printf(" SIGWINCH → PID 300\n\n");
printf("bg %%vim: 把 vim 移到后台\n");
printf(" tcsetpgrp(STDIN, 100)\n");
printf(" 前台进程组恢复为 shell\n");
printf(" vim 继续在后台运行\n");#include <stdio.h>
#include <string.h>
/*
* 进程的组织结构:
* 会话 (Session)
* └── 前台进程组 (Foreground Process Group)
* └── 进程 (Process)
* └── 后台进程组 (Background Process Group)
* └── 进程 (Process)
*
* 关键系统调用:
* setpgid(pid, pgid) — 设置进程组
* setsid() — 创建新会话
* tcsetpgrp(fd, pgid) — 设置前台进程组
*/
struct process {
int pid;
int pgid; /* 进程组 ID */
int sid; /* 会话 ID */
char comm[16];
};
struct session {
int sid;
int fg_pgid; /* 前台进程组 */
struct process *leader;
};
struct process_group {
int pgid;
struct process *procs[8];
int nr_procs;
};
int main() {
printf("=== 进程组与会话 ===\n\n");
/* shell 小妹的会话 */
struct process shell = { .pid = 100, .pgid = 100, .sid = 100, .comm = "bash" };
struct process grep = { .pid = 201, .pgid = 200, .sid = 100, .comm = "grep" };
struct process sort = { .pid = 202, .pgid = 200, .sid = 100, .comm = "sort" };
struct process vim = { .pid = 300, .pgid = 300, .sid = 100, .comm = "vim" };
struct session sess = {
.sid = 100,
.fg_pgid = 200, /* grep | sort 是前台 */
.leader = &shell,
};
printf("会话 SID=100 (leader: %s)\n", shell.comm);
printf(" 前台进程组 PGID=200:\n");
printf(" PID %d (%s)\n", grep.pid, grep.comm);
printf(" PID %d (%s)\n", sort.pid, sort.comm);
printf(" 后台进程组 PGID=300:\n");
printf(" PID %d (%s)\n", vim.pid, vim.comm);
printf("\n--- Ctrl+C 的旅程 ---\n");
printf("1. 用户按下 Ctrl+C\n");
printf("2. 终端驱动发送 SIGINT\n");
printf("3. SIGINT 发送给前台进程组的所有进程\n");
printf(" 前台进程组 PGID=%d\n", sess.fg_pgid);
printf(" → PID %d (%s) 收到 SIGINT\n", grep.pid, grep.comm);
printf(" → PID %d (%s) 收到 SIGINT\n", sort.pid, sort.comm);
printf(" → PID %d (%s) 不受影响(后台)\n", vim.pid, vim.comm);
printf("\n--- fg/bg 命令 ---\n");
printf("fg %%vim: 把 vim 移到前台\n");
printf(" tcsetpgrp(STDIN, 300)\n");
printf(" 前台进程组变为 PGID=300\n");
printf(" SIGWINCH → PID 300\n\n");
printf("bg %%vim: 把 vim 移到后台\n");
printf(" tcsetpgrp(STDIN, 100)\n");
printf(" 前台进程组恢复为 shell\n");
printf(" vim 继续在后台运行\n");
return 0;
}进程也有"家族"和"领地"。
二
林小源在研究会话的过程中,理解了一个更深层的设计。
会话(Session)是进程组的"上级"。一个会话通常对应一个终端。会话的创建者是会话领袖(Session Leader)——通常是 shell。
会话领袖有一个特殊的角色:它负责管理终端。当终端关闭时,内核向会话领袖发送 。会话领袖收到 后,通常会向会话中的所有进程组发送 ,通知它们终端已经关闭。
/*
* 会话领袖的职责:
* 1. 创建会话(setsid())
* 2. 打开终端,成为控制终端
* 3. 管理前台进程组
* 4. 处理终端关闭事件(SIGHUP)
*
* 控制终端 (Controlling Terminal):
* - 每个会话最多有一个控制终端
* - 终端的输入/输出与会话关联
* - 前台进程组接收终端的输入
*/
struct terminal {
int fd;
int session_id;
int fg_pgid;
};
struct session_leader {
int pid;
int sid;
struct terminal *tty;
};
printf("=== 会话与终端 ===\n\n");
struct terminal tty = { .fd = 0, .session_id = 100, .fg_pgid = 200 };
struct session_leader shell = { .pid = 100, .sid = 100, .tty = &tty };
printf("会话领袖: PID %d, SID %d\n", shell.pid, shell.sid);
printf("控制终端: fd=%d\n", shell.tty->fd);
printf("前台进程组: PGID=%d\n\n", shell.tty->fg_pgid);
printf("--- 终端关闭时 ---\n");
printf("1. 用户关闭终端窗口\n");
printf("2. 内核检测到终端关闭\n");
printf("3. 内核向会话领袖 (PID %d) 发送 SIGHUP\n", shell.pid);
printf("4. 会话领袖向所有进程组发送 SIGHUP\n");
printf("5. 前台进程组 (PGID=200) 收到 SIGHUP\n");
printf("6. 进程退出\n\n");
printf("--- setsid() 的作用 ---\n");
printf("创建新会话:\n");
printf(" - 调用者成为会话领袖\n");
printf(" - 调用者成为新进程组的组长\n");
printf(" - 没有控制终端(除非后来打开)\n");
printf("\n");
printf("典型用法:守护进程(daemon)\n");
printf(" 1. fork() → 子进程\n");
printf(" 2. 父进程退出\n");
printf(" 3. 子进程调用 setsid()\n");
printf(" 4. 子进程与原终端脱离\n");
printf(" 5. 子进程在后台独立运行\n");#include <stdio.h>
/*
* 会话领袖的职责:
* 1. 创建会话(setsid())
* 2. 打开终端,成为控制终端
* 3. 管理前台进程组
* 4. 处理终端关闭事件(SIGHUP)
*
* 控制终端 (Controlling Terminal):
* - 每个会话最多有一个控制终端
* - 终端的输入/输出与会话关联
* - 前台进程组接收终端的输入
*/
struct terminal {
int fd;
int session_id;
int fg_pgid;
};
struct session_leader {
int pid;
int sid;
struct terminal *tty;
};
int main() {
printf("=== 会话与终端 ===\n\n");
struct terminal tty = { .fd = 0, .session_id = 100, .fg_pgid = 200 };
struct session_leader shell = { .pid = 100, .sid = 100, .tty = &tty };
printf("会话领袖: PID %d, SID %d\n", shell.pid, shell.sid);
printf("控制终端: fd=%d\n", shell.tty->fd);
printf("前台进程组: PGID=%d\n\n", shell.tty->fg_pgid);
printf("--- 终端关闭时 ---\n");
printf("1. 用户关闭终端窗口\n");
printf("2. 内核检测到终端关闭\n");
printf("3. 内核向会话领袖 (PID %d) 发送 SIGHUP\n", shell.pid);
printf("4. 会话领袖向所有进程组发送 SIGHUP\n");
printf("5. 前台进程组 (PGID=200) 收到 SIGHUP\n");
printf("6. 进程退出\n\n");
printf("--- setsid() 的作用 ---\n");
printf("创建新会话:\n");
printf(" - 调用者成为会话领袖\n");
printf(" - 调用者成为新进程组的组长\n");
printf(" - 没有控制终端(除非后来打开)\n");
printf("\n");
printf("典型用法:守护进程(daemon)\n");
printf(" 1. fork() → 子进程\n");
printf(" 2. 父进程退出\n");
printf(" 3. 子进程调用 setsid()\n");
printf(" 4. 子进程与原终端脱离\n");
printf(" 5. 子进程在后台独立运行\n");
return 0;
}这下他明白了——守护进程就是靠这套操作跟终端彻底"割席"的。
林小源想起了 cron 老伯——那个安静的定时任务守护进程。他就是通过 创建了自己的会话,与终端彻底脱离,然后在后台默默地运行。
"你见过 cron 老伯吗?"林小源问 shell 小妹。
"当然见过。"shell 小妹一边等待用户输入一边说,"他很安静。每天在后台运行,不和任何人说话。我有时候会收到他的信号——他让我执行定时任务。"
"他是怎么和终端脱离的?"
"fork 一次,父进程退出,子进程调用 。"shell 小妹说得轻描淡写,"这样他就不是任何会话的成员了。没有控制终端,没有 ,终端关闭也影响不到他。"
"听起来很孤独。"
shell 小妹想了想。"也许吧。但他选择了这条路。有些进程注定要离开终端,在后台默默运行。"
三
林小源在观察 shell 小妹管理进程组的过程中,看到了一个有趣的场景。
用户在 shell 中运行了一条管道命令:ls | grep foo | sort。shell 小妹 fork 了三个子进程,分别执行 ls、grep 和 。这三个子进程属于同一个进程组(PGID 相同),但它们通过管道连接在一起。
"它们是一个家族吗?"林小源问。
"当然是。"shell 小妹说,"它们的 PGID 相同——都是我设置的。在终端看来,它们是一个整体。"
"那 Ctrl+C 呢?"
"Ctrl+C 会杀死整个前台进程组。"shell 小妹的语气变得严肃了一些。"终端驱动发送 ,不是发给某一个进程,而是发给前台进程组的所有进程。ls、grep、——一个都跑不掉。"
"那后台的进程呢?"
"后台进程不受影响。"shell 小妹说,"只有前台进程组接收终端的输入信号。、、——都只发给前台。后台进程可以安全地继续运行。"
林小源看着那三个通过管道连接的子进程——它们共享同一个 PGID,命运绑定在一起。Ctrl+C 一按,全军覆没。
管道命令是一个"命运共同体"。
shell 小妹似乎看出了他的想法,补充道:"但这也是它们的力量。三个进程各司其职,通过管道协作,完成一个单独进程无法完成的任务。家族的意义就在于此——一起活着,一起战斗,必要时一起赴死。"
道藏笔记
内核启示
进程组和会话是进程的两级组织结构。
会话是上面那层,由 创建,会话领袖通常是 shell。每个会话最多挂一个控制终端,终端关闭时会话领袖会收到 。进程组是下面那层,由 设置——同一管道里的进程属于同一个进程组,组长的 PID 就是 PGID,前台进程组负责接收终端输入。
信号怎么分发呢?(Ctrl+C)、(Ctrl+\)、(Ctrl+Z)都只发给前台进程组,后台进程不受影响。 则是终端关闭时发给会话领袖,领袖再往下传。
守护进程的诞生有一套标准流程:先 让父进程退出,然后 创建新会话跟终端脱钩,再 一次防止会话领袖意外获得控制终端,最后改工作目录、关掉不需要的文件描述符。cron 老伯就是这样炼成的。
进程不是孤立的,它们有家族、有领地、有归属。
家族之试
本章讲进程家族和前台进程组时,Ctrl+C 最终会转化为哪一个信号?