信号的基本使用场景:使用 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.1 INTERRUPTIBLE/UNINTERRUPTIBLE 进程对信号的响应
上节主要描述运行状态(TASK_RUNNING)进程对信号的响应时机:信号发送后挂到目标进程的信号队列,进程返回用户态的时候在 do_notify_resume()
中处理信号。
那么对于阻塞状态的进程又怎么样来响应信号呢?
让一个进程进入阻塞状态,我们可以选择让其进入可中断(TASK_INTERRUPTIBLE)或者不可中断(TASK_UNINTERRUPTIBLE)状态,比如 mutex 操作分为 mutex_lock()
和 mutex_lock_interruptible()
。所谓的可中断和不可中断就是说是否可以被中断信号打断:如果进程处于可中断(TASK_INTERRUPTIBLE)状态,信号发送函数会直接唤醒进程,让进程处理完内核态操作去返回用户态,让进程迅速去执行信号处理函数;如果进程处于不可中断(TASK_UNINTERRUPTIBLE)状态俗称为 D 进程,信号只会挂到信号队列,但是没有机会去立即执行。
- kernel/signal.c:
- __send_signal() -> complete_signal() -> signal_wake_up() -> signal_wake_up_state()
1 | void signal_wake_up_state(struct task_struct *t, unsigned int state) |
1.2内核进程响应信号
上面说到内核进程普通情况下是不会响应信号的,如果需要内核进程响应信号,可以在内核进程中加入如下代码:
1 | if (signal_pending(current)) |
2.信号简介
在给大家引出重点的信号响应时机以后,还是简单介绍以下信号的背景知识。信号也是一种进程间通讯的机制,它传递的信息很短,只有一个编号。
2.1 常规信号和实时信号
Linux 传统的信号 1~31 为常规信号(regular signal),POSIX 还引入了一种新的信号实时信号(real-time signal)编号为 32~64。它们的不同在于:常规信号同一个编号在 pending 队列中只存在一份,如果有重复的则直接丢弃;实时信号的多个相同信号不能丢弃,需要保证每个信号都能送达。
Linux 常用的是常规信号,以下是具体的定义ULK:
编号 | 信号名称 | 缺省操作 | 解释 | POSIX |
---|---|---|---|---|
1 | SIGHUP | Terminate | Hang up controlling terminal or process | Yes |
2 | SIGINT | Terminate | Interrupt from keyboard | Yes |
3 | SIGQUIT | Dump | Quit from keyboard | Yes |
4 | SIGILL | Dump | Illegal instruction | Yes |
5 | SIGTRAP | Dump | Breakpoint for debugging | No |
6 | SIGABRT | Dump | Abnormal termination | Yes |
6 | SIGIOT | Dump | Equivalent to SIGABRT | No |
7 | SIGBUS | Dump | Bus error | No |
8 | SIGFPE | Dump | Floating-point exception | Yes |
9 | SIGKILL | Terminate | Forced-process termination | Yes |
10 | SIGUSR1 | Terminate | Available to processes | Yes |
11 | SIGSEGV | Dump | Invalid memory reference | Yes |
12 | SIGUSR2 | Terminate | Available to processes | Yes |
13 | SIGPIPE | Terminate | Write to pipe with no readers | Yes |
14 | SIGALRM | Terminate | Real-timerclock | Yes |
15 | SIGTERM | Terminate | Process termination | Yes |
16 | SIGSTKFLT | Terminate | Coprocessor stack error | No |
17 | SIGCHLD | Ignore | Child process stopped or terminated, or got signal if traced | Yes |
18 | SIGCONT | Continue | Resume execution, if stopped | Yes |
19 | SIGSTOP | Stop | Stop process execution | Yes |
20 | SIGTSTP | Stop | Stop process issued from tty | Yes |
21 | SIGTTIN | Stop | Background process requires input | Yes |
22 | SIGTTOU | Stop | Background process requires output | Yes |
23 | SIGURG | Ignore | Urgent condition on socket | No |
24 | SIGXCPU | Dump | CPU time limit exceeded | No |
25 | SIGXFSZ | Dump | File size limit exceeded | No |
26 | SIGVTALRM | Terminate | Virtual timer clock | No |
27 | SIGPROF | Terminate | Profile timer clock | No |
28 | SIGWINCH | Ignore | Window resizing | No |
29 | SIGIO | Terminate | I/O now possible | No |
29 | SIGPOLL | Terminate | Equivalent to SIGIO | No |
30 | SIGPWR | Terminate | Power supply failure | No |
31 | SIGSYS | Dump | Bad system call | No |
31 | SIGUNUSED | Dump | Equivalent to SIGSYS | No |
所谓的缺省操作:是在用户没有注册用户态的信号处理函数的情况下,默认的信号内核处理方法。在第4节中会详细的讲解。
3.信号的发送
信号的发送者可以是 user 也可以是 kernel,我们经常是通过用户态来调用 kill()、tkill() 等函数来发送信号的,我们通过分析这些系统调用来理解信号的具体发送过程。
- 与信号相关的系统调用主要有以下函数:
系统调用 | 说明 |
---|---|
kill | 向线程组发送信号 |
tkill | 向进程发送信号 |
tgkill | 向指定线程组中的进程发送信号 |
signal | 注册信号的用户态处理函数 |
sigprocmask | block/unblock信号 |
3.1 kill()
kill()
系统调用的功能是发送一个信号给线程组,只需要线程组挑出一个线程来响应处理信号。但是对于致命信号,线程组内所有进程都会被杀死,而不仅仅是处理信号的线程。
- kernel/signal.c:
- kill() -> kill_something_info() -> kill_pid_info() -> group_send_sig_info() -> do_send_sig_info() -> send_signal() -> __send_signal()
1 | SYSCALL_DEFINE2(kill, pid_t, pid, int, sig) |
接下来来到了发送信号的核心函数 __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()
函数中执行完了。对于用户有注册处理函数的信号,但是因为这些处理函数都是用户态的,所以内核使用了一个技巧:先构造堆栈,返回用户态去执行自定义信号处理函数,再返回内核态继续被信号打断的返回用户态的动作。
我们来看 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 ↩
This is copyright.