目录

Linux Kernel | Linux 信号机制介绍

1. 信号

1.1. 概述

信号(Signal)其实就是 Linux 进程收到的一个通知。这些通知产生的源头有很多种,通知的类型也有很多种。比如

  • 如果我们按下键盘“Ctrl+C”,当前运行的进程就会收到一个信号 SIGINT 而退出;
  • 如果我们的代码写得有问题,导致内存访问出错了,当前的进程就会收到另一个信号 SIGSEGV;
  • 我们也可以通过命令 kill ,直接向一个进程发送一个信号,缺省情况下不指定信号的类型,那么这个信号就是 SIGTERM。也可以指定信号类型,比如命令 “kill -9 “, 这里的 9,就是编号为 9 的信号,SIGKILL 信号。

在 Linux 上我们可以用 kill -l 来看这些信号的编号和名字,具体的编号和名字如下所示:

1
2
3
4
5
6
7
8
$ kill -l
 1) SIGHUP      2) SIGINT    3) SIGQUIT    4) SIGILL    5) SIGTRAP
 6) SIGABRT     7) SIGBUS    8) SIGFPE     9) SIGKILL  10) SIGUSR1
11) SIGSEGV    12) SIGUSR2  13) SIGPIPE   14) SIGALRM  15) SIGTERM
16) SIGSTKFLT  17) SIGCHLD  18) SIGCONT   19) SIGSTOP  20) SIGTSTP
21) SIGTTIN    22) SIGTTOU  23) SIGURG    24) SIGXCPU  25) SIGXFSZ
26) SIGVTALRM  27) SIGPROF  28) SIGWINCH  29) SIGIO    30) SIGPWR
31) SIGSYS

信号这个概念在很早期的 Unix 系统上就有了。它一般会从 1 开始编号,通常来说,信号编号是 1 到 31,这个编号在所有的 Unix 系统上都是一样的。

1.2. 信号的系统调用

信号的两个系统调用:

  • kill() ,用于发送信号;
  • signal(),注册信号的 handler。
1
// sig 表示要发送哪个信号,比如 SIGTERM 是 15;pid 表示发送给哪个进程int kill(pid_t pid, int sig);// signum 信号的编号;handler 是一个函数指针sighandler_t signal(int signum, sighandler_t handler);

kill 这个程序其实是调用 kill 系统调用向线程组或进程组发送信号来终止线程组或者进程组的:

  • pid > 0 ,那么将会调用 kill_pid_info 来向线程 pid 所属的线程组发送信号
  • pid = 0,向当前进程组发送信号
  • pid < -1 ,向组长标识符为 -pid 的进程组发送信号
  • pid = -1,向除 1 号进程和当前线程组以为的所有线程组发送信号

1.3. 信号的处理

进程对信号的处理,有三个选择:

  • 缺省行为,对于每个信号,用户进程如果不注册一个自己的 handler,就会有一个系统缺省的 handler,这个缺省的 handler 叫作 SIG_DFL。系统中不同的缺省行为有:退出、暂停、忽略。man 7 signal 可以查看每个信号的缺省行为。对于大部分的信号而言,应用程序不需要注册自己的 handler,使用系统缺省定义行为就可以了。
  • 捕获,可以让用户进程注册自己针对这个信号的 handler。当信号来了之后会调用 signal() 函数注册的 handler。
  • 忽略,就是对这个信号不做任何处理。可以调用 signal() 这个系统调用为信号注册 SIG_IGN handler,表示就是忽略这个信号。

SIGKILL 和 SIGSTOP 这两个信号是特权信号(特权信号是 Linux 为 kernel 和超级用户去删除任意进程所保留的,不能被忽略也不能被捕获,只能采用缺省的行为) 。SIGSTOP 的默认操作就是暂停进程的执行,SIGKILL 的默认操作是终止进程的执行。

SIGTERM 是 Linux 命令 kill 缺省时候会发出的信号。SIGTERM 这个信号是可以被捕获的,这里的“捕获”指的就是用户进程可以为这个信号注册自己的 handler。

https://img.dawnguo.cn/Linux/cec445b6af1c0f678cc1b538bb03d67f.jpeg

在我们运行 kill 1 这个命令的时候,希望把 SIGTERM 这个信号发送给 1 号进程,就像下面图里的带箭头虚线。那么,在 linux 的实现中,流程如下:

  • kill 命令会调用了 kill() 的这个系统调用(所谓系统调用就是内核的调用接口)而进入到了内核函数 sys_kill(), 也就是下图里的实线箭头。

  • 而内核在决定把信号发送给 1 号进程的时候,会调用 sig_task_ignored() 这个函数来做个判断。它会决定内核在哪些情况下会把发送的这个信号给忽略掉。如果信号被忽略了,那么 init 进程就不能收到指令了。

    在 sig_task_ignored() 这个函数中有三个 if{} 判断,第一个和第三个 if{} 判断和我们的问题没有关系,并且代码有注释,我们就不讨论了。对于第二个 if{} 来说,它有三个子条件,如果这三个子条件都满足了的话,那么这个信号就会被忽略,从而不会发送给进程

    • “t->signal->flags & SIGNAL_UNKILLABLE”,也就是说进程必须是 SIGNAL_UNKILLABLE 的。

      在每个 Namespace 的 init 进程建立的时候,就会打上 SIGNAL_UNKILLABLE 这个标签,也就是说只要是 1 号进程,就会有这个 flag。

      那么这个条件是满足的。

      1
      
      // kernel/fork.c                       if (is_child_reaper(pid)) {                                ns_of_pid(pid)->child_reaper = p;                                p->signal->flags |= SIGNAL_UNKILLABLE;                        }/* * is_child_reaper returns true if the pid is the init process * of the current namespace. As this one could be checked before * pid_ns->child_reaper is assigned in copy_process, we check * with the pid number. */static inline bool is_child_reaper(struct pid *pid){        return pid->numbers[pid->level].nr == 1;}
      
    • “handler == SIG_DFL”,第二个条件判断信号的 handler 是否是 SIG_DFL。对于 SIGKILL,我们前面介绍过它是特权信号,是不允许被捕获的,所以它的 handler 就一直是 SIG_DFL。这第二个条件对 SIGKILL 来说总是满足的。对于 SIGTERM,它是可以被捕获的。也就是说如果用户不注册 handler,那么这个条件对 SIGTERM 也是满足的。

    • “!(force && sig_kernel_only(sig))” ,这个条件里 force 的值,对于同一个 Namespace 里发出的信号来说,调用值是 0,所以这个条件总是满足的。

    1
    
    // kernel/signal.cstatic bool sig_task_ignored(struct task_struct *t, int sig, bool force){        void __user *handler;        handler = sig_handler(t, sig);        /* SIGKILL and SIGSTOP may not be sent to the global init */        if (unlikely(is_global_init(t) && sig_kernel_only(sig)))                return true;        if (unlikely(t->signal->flags & SIGNAL_UNKILLABLE) &&            handler == SIG_DFL && !(force && sig_kernel_only(sig)))                return true;        /* Only allow kernel generated signals to this kthread */        if (unlikely((t->flags & PF_KTHREAD) &&                     (handler == SIG_KTHREAD_KERNEL) && !force))                return true;        return sig_handler_ignored(handler, sig);}
    

1.4. 信号的注册情况

查看进程状态中的 sigcgt bitmap 可以知道这个进程注册 handler 的情况。需要注意的是:不同编程语言编译出来的程序针对不同的信号注册 handler 的情况会有所不同。

  • 在 Golang 程序里,很多信号都注册了自己的 handler,当然也包括了 SIGTERM(15),也就是 bit 15。
  • C 程序里,缺省状态下,一个信号 handler 都没有注册。bash 程序里注册了两个 handler,bit 2 和 bit 17,也就是 SIGINT 和 SIGCHLD,但是没有注册 SIGTERM。所以,C 程序和 bash 程序里 SIGTERM 的 handler 是 SIG_DFL(系统缺省行为),那么它们就不能被 SIGTERM 所杀。
1
### golang init# cat /proc/1/status | grep -i SigCgtSigCgt:     fffffffe7fc1feff### C init# cat /proc/1/status | grep -i SigCgtSigCgt:     0000000000000000### bash init# cat /proc/1/status | grep -i SigCgtSigCgt:     0000000000010002