信号的基本使用场景:使用“ctrl+c”中止一个程序,或者使用”kill pid”命令杀掉一个进程。
Linux信号机制基本上每个同学都用过,但是信号的具体实现机制还是有很多人不清楚的。在很多人的概念中信号是一种异步机制,像中断一样。但是除了硬中断,信号也是由中断实现的吗?如果不是中断,系统又怎么样来利用软件机制模拟类似如异步中断的动作?
本文的代码分析基于linux kernel 3.18.22,最好的学习方法还是”read the fucking source code”
1.信号的响应时机
理解信号异步机制的关键是信号的响应时机,我们对一个进程发送一个信号以后,其实并没有硬中断发生,只是简单把信号挂载到目标进程的信号pending队列上去,信号真正得到执行的时机是进程执行完异常/中断返回到用户态的时刻。
让信号看起来是一个异步中断的关键就是,正常的用户进程是会频繁的在用户态和内核态之间切换的(这种切换包括:系统调用、缺页异常、系统中断…),所以信号能很快的能得到执行。
这也带来了一点问题,内核进程是不响应信号的,除非它刻意的去查询。所以通常情况下我们无法通过kill命令去杀死一个内核进程。
- arch/arm64/kernel/entry.s:
- el0_sync()/el0_irq() -> ret_to_user() -> work_pending() -> do_notify_resume()
1 | // (1) 在arm64架构中,kernel运行在el1,用户态运行在el0。 |
- arch/arm64/kernel/signal.c:
- -> do_notify_resume() -> do_signal() -> get_signal()/handle_signal()
1 | asmlinkage void do_notify_resume(struct pt_regs *regs, |
1.2 内核进程响应信号
上面说到内核进程普通情况下是不会响应信号的,如果需要内核进程响应信号,可以在内核进程中加入如下代码:
1 | if (signal_pending(current)) |
[^ULK]
接下来来到了发送信号的核心函数__send_signal(),函数的主要目的是把信号挂到信号的pending队列中去。
pending队列有两种:一种是进程组共享的task_struct->signal->>shared_pending,发送给线程组的信号会挂载到该队列;另一种是进程私有队列task_struct–>pending,发送给进程的信号会挂载到该队列。
从下面的代码中,我们可以看到在创建线程时,线程组贡献信号队列task_struct-> signal-> shared_pending是怎么实现的。
- kernel/fork.c:
- do_fork() -> copy_process() -> copy_signal()/copy_sighand()
1 | static struct task_struct *copy_process(unsigned long clone_flags, |
继续来看__send_signal()的具体实现:
- kernel/signal.c:
- -> __send_signal() -> prepare_signal()/complete_signal()
1 | static int __send_signal(int sig, struct siginfo *info, struct task_struct *t, |
3.2 tkill()
kill()是向进程组发一个信号,而tkill()是向某一个进程发送信号。
- kernel/signal.c:
- tkill() -> do_tkill() -> do_send_specific() -> send_signal()
1 | SYSCALL_DEFINE2(tkill, pid_t, pid, int, sig) |
3.3 tgkill()
tgkill()是向特定线程组中的进程发送信号。
- kernel/signal.c:
- tkill() -> do_tkill() -> do_send_specific() -> send_signal()
1 | SYSCALL_DEFINE3(tgkill, pid_t, tgid, pid_t, pid, int, sig) |
3.4 signal()
signal()/sigaction()注册用户自定义的信号处理函数。
- kernel/signal.c:
- signal() -> do_sigaction()
1 | SYSCALL_DEFINE2(signal, int, sig, __sighandler_t, handler) |
3.5 sigprocmask()
sigprocmask()用来设置进程对信号是否阻塞。阻塞以后,信号继续挂载到信号pending队列,但是信号处理时不响应信号。
SIG_BLOCK命令阻塞信号,SIG_UNBLOCK命令解除阻塞信号。
- kernel/signal.c:
- sigprocmask() -> set_current_blocked()
1 | SYSCALL_DEFINE3(sigprocmask, int, how, old_sigset_t __user *, nset, |
关于信号阻塞current->blocked的使用在信号处理函数get_signal()中使用。
- arch/arm64/kernel/signal.c:
- do_signal() -> get_signal()
1 | int get_signal(struct ksignal *ksig) |
4.信号的处理
- 系统对信号的处理有三种方式:
信号响应 | 描述 |
---|---|
忽略 | ignore |
调用用户态注册的处理函数 | 如果用户有注册信号处理函数,调用sighand->action[signr-1]中对应的注册函数 |
调用默认的内核态处理函数 | 如果用户没有注册对应的处理函数,调用默认的内核处理 |
- 默认的内核态处理,进一步可以细分为几种:
信号默认内核处理类型 | 描述 |
---|---|
Terminate | 进程被中止(杀死)。 |
Dump | 进程被中止(杀死),并且输出dump文件。 |
Ignore | 信号被忽略。 |
Stop | 进程被停止,把进程设置为TASK_STOPPED状态。 |
Continue | 如果进程被停止(TASK_STOPPED) ,把它设置成TASK_RUNNING状态。 |
4.1 do_signal()
信号处理的核心函数就是do_signal(),下面我们来详细分析一下具体实现。
- arch/arm64/kernel/signal.c:
- -> ret_to_user() -> do_notify_resume() -> do_signal() -> get_signal()/handle_signal()
1 | static void do_signal(struct pt_regs *regs) |
如果用户没有注册信号处理函数,默认的内核处理函数在get_signal()函数中执行完了。对于用户有注册处理函数的信号,但是因为这些处理函数都是用户态的,所以内核使用了一个技巧:先构造堆栈,返回用户态去执行自定义信号处理函数,再返回内核态继续被信号打断的返回用户态的动作。
[^ULK]
我们来看handle_signal()函数中的具体实现。
- arch/arm64/kernel/signal.c:
- -> ret_to_user() -> do_notify_resume() -> do_signal() -> handle_signal()
1 | static void handle_signal(struct ksignal *ksig, struct pt_regs *regs) |
参考资料
[^ULK]: Understanding the Linux Kernel