Skip to content

第二十六章:不可阻挡

筑基中期

涉及内核源码:

林小源在信号之河的上游行走时,撞上了一堵墙。

不是比喻——他真的撞了上去。那堵墙从内核的深处拔地而起,通体漆黑,表面刻满了密密麻麻的信号编号。他伸手触碰,指尖刚触及墙面,一股排斥的力量便将他弹开。

"什么东西……"他揉了揉发麻的手掌。

墙上刻着两个数字:919

他试着绕过去,但墙延伸到视线尽头,没有缝隙。他试着用信号投射的方式向墙发送了一条消息——石沉大海,毫无回应。

"别费力气了。"一个沙哑的声音从墙后传来。

林小源循声望去,看到墙的表面浮现出一张模糊的面孔。那面孔没有表情,像是一段被硬编码的逻辑。

"你是谁?"

"我是 。"那面孔说,"你撞上的墙叫 。在我这里,它永远不会被忽略。"

"永远不会?"

"永远不会。"面孔的语气没有波动,像机器一样精确。"任何进程——无论它注册了什么处理器,无论它设置了什么掩码——收到 就必须死。 对它返回 -EINVAL,信号掩码对它无效,忽略策略对它不适用。"

林小源盯着墙上的数字 9,感到一阵寒意。这不是普通的信号——这是内核给管理员的最后手段。

c
/*
 * SIGKILL 和 SIGSTOP 的特殊性:
 * 1. 不能被信号处理器捕获
 * 2. 不能被信号掩码阻塞
 * 3. 不能被忽略
 *
 * 内核在 sig_task_ignored() 中检查:
 * 如果信号是 SIGKILL 或 SIGSTOP,直接返回 false
 * 意味着信号永远不会被"忽略"——必须执行默认行为
 */

#define SIGKILL  9
#define SIGSTOP 19
#define SIGTERM 15

struct sigaction {
    void (*handler)(int);
    unsigned long flags;
    unsigned long mask;
};

int do_sigaction(int sig, struct sigaction *act) {
    /* SIGKILL 和 SIGSTOP 不能被捕获 */
    if (sig == SIGKILL || sig == SIGSTOP) {
        return -1;  /* -EINVAL */
    }
    return 0;
}

struct sigaction act = { .handler = (void *)1, .flags = 0, .mask = 0 };

printf("=== SIGKILL 的不可阻挡性 ===\n\n");

int signals[] = { SIGTERM, SIGKILL, SIGSTOP };
const char *names[] = { "SIGTERM", "SIGKILL", "SIGSTOP" };

for (int i = 0; i < 3; i++) {
    int ret = do_sigaction(signals[i], &act);
    printf("sigaction(%s): %s\n", names[i],
           ret == 0 ? "注册成功" : "拒绝(EINVAL)");
}

printf("\n--- 为什么不可阻挡? ---\n");
printf("SIGKILL:  给管理员一个\"绝对\"的终止手段\n");
printf("           如果进程能捕获 SIGKILL,恶意程序可以永远不死\n");
printf("SIGSTOP:  给管理员一个\"绝对\"的暂停手段\n");
printf("           如果进程能捕获 SIGSTOP,它可以拒绝被调试\n");

printf("\n--- 内核实现 ---\n");
printf("sig_task_ignored():\n");
printf("  if (sig == SIGKILL || sig == SIGSTOP)\n");
printf("      return false;  // 不可忽略\n");
printf("\n");
printf("SIG_KERNEL_ONLY_MASK:\n");
printf("  #define SIG_KERNEL_ONLY_MASK (sigmask(SIGKILL) | sigmask(SIGSTOP))\n");
printf("  任何对这个掩码的操作都被内核拒绝\n");

"那 呢?"林小源问。

"19号。暂停执行令。"面孔说,"和 一样不可捕获。调试器需要它——如果进程能挡住 ,它就能拒绝被调试。"

林小源沉默了。他终于理解了这堵墙存在的意义:绝对的权力,是为了绝对的秩序。如果一个恶意程序可以捕获 ,它就能永远不死;如果它可以捕获 ,它就能永远不被检查。内核不能允许这种事发生。

林小源沿着墙走了很远,忽然发现墙上有一处凹陷。

凹陷里嵌着一个金色的徽章,上面刻着数字 1

"这是……"

"PID 1。" 的面孔再次浮现,但这次它的语气有了一丝不同——像是在陈述一个特殊条款。" 对它无效。"

林小源瞪大了眼睛。"无效?你不是说永远不会被忽略吗?"

"对所有进程——除了它。"面孔说," 检查目标进程是否是 init。如果是, 被忽略。这是硬编码的例外。"

"为什么?"

面孔沉默了一瞬。"因为 init 死了,整个用户态世界就塌了。"

林小源想起了 init 童子。那个曾经嘲笑他的存在,那个语气里带着疲惫和傲慢的 PID 1。此刻他忽然明白了——那不是傲慢,是重压。

c
/*
 * SIGKILL 对 PID 1 的特殊处理:
 * 在 sig_task_ignored() 中:
 *   if (is_global_init(tsk))  // PID 1
 *       return true;           // 忽略 SIGKILL
 *
 * 这保证了 init 进程永远不会被杀死。
 * 如果 init 死了,整个用户态世界崩溃。
 */

#define SIGKILL 9

struct task_struct {
    int pid;
    char comm[16];
    int is_init;
};

int sig_task_ignored(struct task_struct *tsk, int sig) {
    /* PID 1 对 SIGKILL 免疫 */
    if (tsk->is_init && sig == SIGKILL) {
        return 1;  /* 忽略 */
    }
    return 0;
}

struct task_struct procs[] = {
    { .pid = 1,    .comm = "init",    .is_init = 1 },
    { .pid = 100,  .comm = "shell",   .is_init = 0 },
    { .pid = 200,  .comm = "worker",  .is_init = 0 },
};

printf("=== SIGKILL 对不同进程的效果 ===\n\n");

for (int i = 0; i < 3; i++) {
    int ignored = sig_task_ignored(&procs[i], SIGKILL);
    printf("SIGKILL → PID %d (%s): %s\n",
           procs[i].pid, procs[i].comm,
           ignored ? "被忽略(金身护体)" : "执行默认行为(终止)");
}

printf("\n--- 为什么 PID 1 有金身? ---\n");
printf("init 是所有孤儿进程的父进程\n");
printf("init 负责回收僵尸进程\n");
printf("init 管理系统服务的启动和停止\n");
printf("如果 init 死了:\n");
printf("  - 孤儿进程无人收养\n");
printf("  - 僵尸进程堆积\n");
printf("  - 系统服务无法管理\n");
printf("  - 整个用户态世界崩溃\n");

init 童子不能被杀死,不能退出,不能崩溃。他必须永远运行。回收所有孤儿进程,清理所有僵尸,管理所有系统服务。

"他的金身,"林小源低声说,"是用责任铸就的。"

的面孔没有回应。墙上的金色徽章静静发光。

林小源在墙边坐了很久,思考着一个问题。

"你说 不可阻挡,"他对 的面孔说,"但进程不可能瞬间死亡。它正在执行代码,正在持有锁,正在写文件——你不可能直接把它的 撕碎。"

面孔第一次露出了类似微笑的表情。"你观察得很仔细。"

"那 是怎么执行的?"

"延迟处理。"面孔说," 不会直接调用 。它设置一个标志————然后等待。当进程从内核态返回用户态时,它会检查这个标志。如果标志被设置,内核调用 ,终止整个线程组。"

林小源眨了眨眼。"连死刑都有程序?"

"当然。"面孔说,"终止过程需要释放资源、通知父进程、刷新文件缓冲区——这些操作不能在中断上下文中完成。如果在中断上下文中强行终止,可能导致锁不释放、数据不一致,甚至内核崩溃。"

"所以即使是 ,也要等到进程回到安全的状态才执行。"

"对。"面孔说,"内核设计的原则之一:不在中断上下文中做复杂操作。即使是天塌下来的事,也要等进程回到用户态再说。"

林小源站起来,拍了拍衣袍上的灰尘。他回头看了一眼那堵不可逾越的墙——,两个不可捕获、不可阻塞、不可忽略的信号。

它们的存在不是为了霸道,而是为了最后的保障。


道藏笔记

内核启示

是内核中唯一不可被捕获、阻塞或忽略的信号。

它们的特殊性由内核硬编码保证:

  • / 返回 -EINVAL
  • 定义了这两个信号的掩码,任何对这个掩码的操作都被拒绝
  • / 直接返回

PID 1 的特殊保护:

  • 检查目标进程是否是 init
  • 如果是, 被忽略
  • 这保证了 init 进程永远不会被杀死

的处理流程:

  1. 设置 标志
  2. 进程返回用户态时检查标志
  3. 调用 终止整个线程组
  4. 释放资源,变成僵尸

绝对的权力,是为了绝对的秩序。


破关试炼

不可阻挡之试

本章强调不能被捕获、不能被忽略的终止信号是哪一个?

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

以修仙之名,悟内核之道