0%

《趣谈linux操作系统》小结(三十一) - 信号

信号

信号其实是一个很像中断的机制,事先注册号信号的处理函数,或者使用默认的处理函数, 当接收到奥信号的时候,调用对应的处理函数进行处理。中断呢,系统默认有一个中断向量表,存储中断处理程序,当接收到中断的时候,从中断向量表中获取中断处理程序进行处理。只是中断是在内核态处理,信号是在用户态处理。

信号的类型

操作系统中,为了响应各种各样的事件,也是定义了非常多的信号。我们可以通过 kill -l 命令,查看所有的信号。

1
2
3
4
5
6
7
8
9
10
11

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

上面的命令是在mac下面敲的,具体到linux显示会不一样,但是内容是代表一样的意义,都是信号的类型。具体类型的说明, 可以通过 ‘man signal’查看。

信号处理

一旦有信号产生,我们就有下面这几种,用户进程对信号的处理方式。

1.执行默认操作。Linux 对每种信号都规定了默认操作

2.捕捉信号。我们可以为信号定义一个信号处理函数。当信号发生时,我们就执行相应的信号处理函数。

3.忽略信号。当我们不希望处理某些信号的时候,就可以忽略该信号,不做任何处理。有两个信号是应用进程无法捕捉和忽略的,即 SIGKILL 和 SEGSTOP,它们用于在任何时候中断或结束某一进程。

如果我们不想让某个信号执行默认操作,一种方法就是对特定的信号注册相应的信号处理函数,设置信号处理方式的是 signal 函数。我们在 Linux 下面执行 man signal 的话,会发现 Linux 不建议我们直接用这个方法,而是改用 sigaction。这两个函数的区别就是sigaction掌控的粒度更小,更灵活。而signal没有那么灵活而已。具体使用的时候,可以去看看函数的说明,决定使用哪个。

信号发送

有时候,我们在终端输入某些组合键的时候,会给进程发送信号,例如,Ctrl+C 产生 SIGINT 信号,Ctrl+Z 产生 SIGTSTP 信号。有的时候,硬件异常也会产生信号。比如,执行了除以 0 的指令,CPU 就会产生异常,然后把 SIGFPE 信号发送给进程。再如,进程访问了非法内存,内存管理模块就会产生异常,然后把信号 SIGSEGV 发送给进程。

对于硬件触发的,无论是中断,还是信号,肯定是先到内核的,然后内核对于中断和信号处理方式不同。一个是完全在内核里面处理完毕,一个是将信号放在对应的进程 task_struct 里信号相关的数据结构里面,然后等待进程在用户态去处理。当然有些严重的信号,内核会把进程干掉。但是,这也能看出来,中断和信号的严重程度不一样,信号影响的往往是某一个进程,处理慢了,甚至错了,也不过这个进程被干掉,而中断影响的是整个系统。一旦中断处理中有了 bug,可能整个 Linux 都挂了。

有时候,内核在某些情况下,也会给进程发送信号。例如,向读端已关闭的管道写数据时产生 SIGPIPE 信号,当子进程退出时,我们要给父进程发送 SIG_CHLD 信号等。

最直接的发送信号的方法就是,通过命令 kill 来发送信号了。

另外,我们还可以通过 kill 或者 sigqueue 系统调用,发送信号给某个进程,也可以通过 tkill 或者 tgkill 发送信号给某个线程。虽然方式多种多样,但是最终都是调用了 do_send_sig_info 函数,将信号放在相应的 task_struct 的信号数据结构中。

  • kill->kill_something_info->kill_pid_info->group_send_sig_info->do_send_sig_info

  • tkill->do_tkill->do_send_specific->do_send_sig_info

  • tgkill->do_tkill->do_send_specific->do_send_sig_info

  • rt_sigqueueinfo->do_rt_sigqueueinfo->kill_proc_info->kill_pid_info->group_send_sig_info->do_send_sig_info

当接收到信号的时候,内核会把信号放到task_struct结构里面, <32的信号放在信号集里面, 其他信号放在一个链表里面。 对于<32的信号可能会丢失,即不可信信号,其他的就是可信的信号。有信号之后,会置一个标志位,当任务从中断或者系统调用返回后,检查标志然后进行处理。

信号处理

从系统调用或者中断返回的时候,都会调用 exit_to_usermode_loop函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18


static void exit_to_usermode_loop(struct pt_regs *regs, u32 cached_flags)
{
while (true) {
......
if (cached_flags & _TIF_NEED_RESCHED)
schedule();
......
/* deal with pending signal delivery */
if (cached_flags & _TIF_SIGPENDING)
do_signal(regs);
......
if (!(cached_flags & EXIT_TO_USERMODE_LOOP_FLAGS))
break;
}
}

如果检查有信号标志_TIF_SIGPENDING, 就调用do_signal函数进行信号处理。 这个时候,我们要看,是否从系统调用中返回。如果是从系统调用返回的话,还要区分我们是从系统调用中正常返回,还是在一个非运行状态的系统调用中,因为会被信号中断而返回。如果是因为信号中断而返回的话, 会在栈桢里面添加中断信号相关的上下文,然后出栈直接调到信号处理函数,而不会调用系统调用前的代码行。

万变不离其宗,其实如果有一些基础的系统知识,不如这里知道栈的用处,那么读代码的时候就比较容易理解了。

行动,才不会被动!

欢迎关注个人公众号 微信 -> 搜索 -> fishmwei,沟通交流。