Skip to content

第二十四章:飞剑传书

筑基中期

涉及内核源码:

林小源第一次真正"感受"到了信号。

那是一道 ——从一个刚退出的子进程发向它的父进程。信号在内核中传播的速度极快——几乎是一瞬间,父进程的 pending 信号队列中就多了一个待处理的信号。

飞剑传书。

信号是进程间通信的最简单方式。它不传递数据——只传递一个整数编号和一个 结构体。但它的威力不可小觑: 可以杀死任何进程(除了 init), 可以暂停任何进程, 报告内存访问违规。

c
/*
 * 常见信号及其默认行为。
 * 每个信号都有一个默认行为:
 *   Term — 终止进程
 *   Core — 终止并转储核心
 *   Stop — 暂停进程
 *   Cont — 继续进程
 *   Ign  — 忽略
 */

struct signal_info {
    int nr;
    const char *name;
    const char *action;
    const char *usage;
};

struct signal_info signals[] = {
    { 1,  "SIGHUP",    "Term",  "终端挂断" },
    { 2,  "SIGINT",    "Term",  "Ctrl+C" },
    { 3,  "SIGQUIT",   "Core",  "Ctrl+\\" },
    { 6,  "SIGABRT",   "Core",  "abort()" },
    { 9,  "SIGKILL",   "Term",  "不可阻挡的死刑" },
    { 11, "SIGSEGV",   "Core",  "段错误" },
    { 15, "SIGTERM",   "Term",  "体面的终结请求" },
    { 17, "SIGCHLD",   "Ign",   "子进程退出通知" },
    { 19, "SIGSTOP",   "Stop",  "不可阻挡的暂停" },
    { 20, "SIGTSTP",   "Stop",  "Ctrl+Z" },
    { 18, "SIGCONT",   "Cont",  "继续执行" },
};
int nr = sizeof(signals) / sizeof(signals[0]);

printf("=== 常见信号 ===\n\n");

for (int i = 0; i < nr; i++) {
    printf("  %-10s (nr=%2d)  %-6s  %s\n",
           signals[i].name, signals[i].nr,
           signals[i].action, signals[i].usage);
}

printf("\n--- 关键区别 ---\n");
printf("SIGKILL:  不可被捕获、阻塞或忽略。只能执行默认行为(终止)\n");
printf("SIGSTOP:  不可被捕获、阻塞或忽略。只能执行默认行为(暂停)\n");
printf("SIGTERM:  可以被捕获。进程可以选择优雅地退出\n");
printf("SIGCHLD:  默认忽略。父进程可以注册处理器来 wait()\n");

林小源望着信号表,心中浮现出一个画面:每一道信号都是一把飞剑,从一个进程射向另一个进程。飞剑上没有数据,只有一张纸条——纸条上写着一个数字,代表信号的类型。

"飞剑传书。"他低声说。

"你这个比喻倒是有意思。"一个尖锐的声音从信号队列的方向传来。林小源抬头一看,一个浑身散发着金色光芒的矮小身影正站在一个位图旁边——那是信号守卫者,名叫"剑侍"。他手里握着无数把微型飞剑,每一把都刻着一个信号编号。

"但你只说对了一半。"剑侍的声音很脆,"飞剑传书,传的是信息,不是数据。信号不能携带任意数据——它只传递一个编号和一个 包含了一些额外信息——比如是谁发的信号、为什么发——但和管道、socket 比起来,信号能传递的信息少得可怜。"

"那信号有什么用?"

"通知。"剑侍简洁地说,"信号的本质是通知——告诉目标进程'某件事发生了'。子进程退出了——SIGCHLD。用户按了 Ctrl+C——SIGINT。内存访问违规——SIGSEGV。它不传数据,它传事件。"

信号的内核实现比林小源想象的要复杂。

发送信号的函数是 。它不是直接把信号"投递"到目标进程——它只是在目标进程的 pending 信号队列中设置一个标志位。信号的真正处理发生在目标进程返回用户态的那一刻。

剑侍解释道:"发送信号很快——只需要设置一个标志位。但处理信号不急。内核不会在信号发送的那一刻就中断目标进程——它等目标进程从内核态返回用户态时才检查 pending 队列。"

"为什么要延迟?"

"因为信号处理器可能需要修改进程的状态——释放资源、写日志、发送消息。这些操作不能在中断上下文中完成——中断上下文不能睡眠。所以必须等到进程返回用户态时才能执行。"剑侍顿了顿,"而且,延迟处理避免了一个问题:如果信号处理器在中断上下文中执行,它不能再被另一个信号中断——那就会导致嵌套信号处理的混乱。"

林小源在信号的延迟处理设计中看到了一种"时机"的智慧。信号的发送和处理被分离开来——发送方只设置标志,处理发生在接收方返回用户态时。这种设计避免了在中断上下文中执行复杂的信号处理器,也避免了嵌套信号处理的问题。

剑侍补充道:"还有一个好处——如果目标进程正在处理一个信号,新的信号会被推迟。 定义了处理器执行期间额外阻塞的信号。这样信号处理器不会被自己中断——除非你用了 标志。"

林小源在研究信号的过程中,注意到了 init 童子的另一个特质。

init 进程(PID 1)对信号有特殊处理。 对 init 无效——这是内核硬编码的规则。如果 init 可以被 杀死,整个用户态世界就会崩溃。所以内核在信号发送路径中检查:如果目标是 PID 1, 被忽略。

林小源把这个发现告诉了剑侍。剑侍点了点头,语气变得严肃起来:

"不只是 对 init 也无效。init 不能被杀死,不能被暂停。它必须永远运行——从系统启动到系统关闭,一刻不停。"

"那 init 不是自由的。"林小源说。

"自由?"剑侍冷笑了一声,"PID 1 不是特权——是枷锁。他不能退出,不能被杀死,不能犯错。他的每一个决定都会影响整个用户态世界。他 fork 出所有用户进程,他回收所有僵尸,他处理所有孤儿进程。他的 信号处理器大概是他身上最忙的部分。"

林小源沉默了。他想起了 init 童子的冷淡——也许那不是傲慢,而是一种疲惫。一个不能犯错、不能休息、不能退出的进程,怎么可能有心情和别人寒暄?

也许他的傲慢,只是一种自我保护。

剑侍看了林小源一眼,说:"你开始理解他了。这很好。但你也要记住——信号不只是通知。它是内核控制用户态进程的手段。 是死刑, 是无期徒刑, 是体面的退场请求。每一道信号都有它的分量。"

林小源把这句话记在了心里。飞剑传书,传的不只是信息——还有权力。


道藏笔记

内核启示

信号是进程间通信的最简单方式。

信号的发送路径:

  1. 在目标进程的 pending 队列中设置标志
  2. 如果目标进程在睡眠中,唤醒它
  3. 信号的处理发生在目标进程返回用户态时
  4. 内核检查 pending 信号,调用信号处理器或执行默认行为

有几个特殊信号值得单独说: 是内核的底线,不能被捕获、阻塞或忽略。 对 PID 1 也无效——内核硬编码保护 init。 默认被忽略,但父进程可以注册处理器来回收子进程。

信号是"延迟处理"的——发送方只设置标志,处理发生在接收方返回用户态时。这种设计避免了在中断上下文中执行复杂的信号处理器。

飞剑传书,传的是信息,不是数据。


破关试炼

飞剑之试

本章“飞剑传书”里,哪一个信号代表不可商量的强制终止?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道