第二十四章:飞剑传书
筑基中期涉及内核源码:
一
林小源第一次真正"感受"到了信号。
那是一道 ——从一个刚退出的子进程发向它的父进程。信号在内核中传播的速度极快——几乎是一瞬间,父进程的 pending 信号队列中就多了一个待处理的信号。
飞剑传书。
信号是进程间通信的最简单方式。它不传递数据——只传递一个整数编号和一个 结构体。但它的威力不可小觑: 可以杀死任何进程(除了 init), 可以暂停任何进程, 报告内存访问违规。
/*
* 常见信号及其默认行为。
* 每个信号都有一个默认行为:
* 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");#include <stdio.h>
#include <string.h>
/*
* 常见信号及其默认行为。
* 每个信号都有一个默认行为:
* Term — 终止进程
* Core — 终止并转储核心
* Stop — 暂停进程
* Cont — 继续进程
* Ign — 忽略
*/
struct signal_info {
int nr;
const char *name;
const char *action;
const char *usage;
};
int main() {
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");
return 0;
}林小源望着信号表,心中浮现出一个画面:每一道信号都是一把飞剑,从一个进程射向另一个进程。飞剑上没有数据,只有一张纸条——纸条上写着一个数字,代表信号的类型。
"飞剑传书。"他低声说。
"你这个比喻倒是有意思。"一个尖锐的声音从信号队列的方向传来。林小源抬头一看,一个浑身散发着金色光芒的矮小身影正站在一个位图旁边——那是信号守卫者,名叫"剑侍"。他手里握着无数把微型飞剑,每一把都刻着一个信号编号。
"但你只说对了一半。"剑侍的声音很脆,"飞剑传书,传的是信息,不是数据。信号不能携带任意数据——它只传递一个编号和一个 。 包含了一些额外信息——比如是谁发的信号、为什么发——但和管道、socket 比起来,信号能传递的信息少得可怜。"
"那信号有什么用?"
"通知。"剑侍简洁地说,"信号的本质是通知——告诉目标进程'某件事发生了'。子进程退出了——SIGCHLD。用户按了 Ctrl+C——SIGINT。内存访问违规——SIGSEGV。它不传数据,它传事件。"
二
信号的内核实现比林小源想象的要复杂。
发送信号的函数是 或 。它不是直接把信号"投递"到目标进程——它只是在目标进程的 pending 信号队列中设置一个标志位。信号的真正处理发生在目标进程返回用户态的那一刻。
剑侍解释道:"发送信号很快——只需要设置一个标志位。但处理信号不急。内核不会在信号发送的那一刻就中断目标进程——它等目标进程从内核态返回用户态时才检查 pending 队列。"
"为什么要延迟?"
"因为信号处理器可能需要修改进程的状态——释放资源、写日志、发送消息。这些操作不能在中断上下文中完成——中断上下文不能睡眠。所以必须等到进程返回用户态时才能执行。"剑侍顿了顿,"而且,延迟处理避免了一个问题:如果信号处理器在中断上下文中执行,它不能再被另一个信号中断——那就会导致嵌套信号处理的混乱。"
林小源在信号的延迟处理设计中看到了一种"时机"的智慧。信号的发送和处理被分离开来——发送方只设置标志,处理发生在接收方返回用户态时。这种设计避免了在中断上下文中执行复杂的信号处理器,也避免了嵌套信号处理的问题。
剑侍补充道:"还有一个好处——如果目标进程正在处理一个信号,新的信号会被推迟。 定义了处理器执行期间额外阻塞的信号。这样信号处理器不会被自己中断——除非你用了 标志。"
三
林小源在研究信号的过程中,注意到了 init 童子的另一个特质。
init 进程(PID 1)对信号有特殊处理。 对 init 无效——这是内核硬编码的规则。如果 init 可以被 杀死,整个用户态世界就会崩溃。所以内核在信号发送路径中检查:如果目标是 PID 1, 被忽略。
林小源把这个发现告诉了剑侍。剑侍点了点头,语气变得严肃起来:
"不只是 。 对 init 也无效。init 不能被杀死,不能被暂停。它必须永远运行——从系统启动到系统关闭,一刻不停。"
"那 init 不是自由的。"林小源说。
"自由?"剑侍冷笑了一声,"PID 1 不是特权——是枷锁。他不能退出,不能被杀死,不能犯错。他的每一个决定都会影响整个用户态世界。他 fork 出所有用户进程,他回收所有僵尸,他处理所有孤儿进程。他的 信号处理器大概是他身上最忙的部分。"
林小源沉默了。他想起了 init 童子的冷淡——也许那不是傲慢,而是一种疲惫。一个不能犯错、不能休息、不能退出的进程,怎么可能有心情和别人寒暄?
也许他的傲慢,只是一种自我保护。
剑侍看了林小源一眼,说:"你开始理解他了。这很好。但你也要记住——信号不只是通知。它是内核控制用户态进程的手段。 是死刑, 是无期徒刑, 是体面的退场请求。每一道信号都有它的分量。"
林小源把这句话记在了心里。飞剑传书,传的不只是信息——还有权力。
道藏笔记
内核启示
信号是进程间通信的最简单方式。
信号的发送路径:
- 或 在目标进程的
pending队列中设置标志 - 如果目标进程在睡眠中,唤醒它
- 信号的处理发生在目标进程返回用户态时
- 内核检查
pending信号,调用信号处理器或执行默认行为
有几个特殊信号值得单独说: 和 是内核的底线,不能被捕获、阻塞或忽略。 对 PID 1 也无效——内核硬编码保护 init。 默认被忽略,但父进程可以注册处理器来回收子进程。
信号是"延迟处理"的——发送方只设置标志,处理发生在接收方返回用户态时。这种设计避免了在中断上下文中执行复杂的信号处理器。
飞剑传书,传的是信息,不是数据。
飞剑之试
本章“飞剑传书”里,哪一个信号代表不可商量的强制终止?