本文转自:http://blog.csdn.net/walkingman321/article/details/6167435
本文介绍了Linux信号处理的基本流程。关于信号处理的具体细节可以看ULK第三版第11章。
1. 基本数据结构
1.1task_struct中信号相关的域
- struct signal_struct * signal; // Pointer to the process's signal descriptor
- struct sighand_struct * sighand; // Pointer to the process's signal handler descriptor
- sigset_t blocked; // Mask of blocked signals
- sigset_t real_blocked; // Temporary mask of blocked signals (used by the // rt_sigtimedwait( ) system call)
- struct sigpending pending; // Data structure storing the private pending signals
- unsigned long sas_ss_sp; // Address of alternative signal handler stack
- size_t sas_ss_size; // Size of alternative signal handler stack
- int (*) (void *) notifier; // Pointer to a function used by a device driver to block some // signals of the process
- void * notifier_data; // Pointer to data that might be used by the notifier function // (previous field of table)
- sigset_t * notifier_mask; // Bit mask of signals blocked by a device driver through a // notifier function
l signal和sighand都是指针,因为同一个进程中所有线程都共享同一个signal和同一个sighand。
l sas_ss_sp和sas_ss_size可以用于定义信号处理函数使用的栈,但不是必须。
1.2sigpending与sigqueue
sigpending代表目前等待处理的信号的集合。
- struct sigpending {
- struct list_head list; // sigqueue链表
- sigset_t signal; // 待处理的信号集合
- };
sigpending结构体中,signal域保存所有待处理信号的集合,每个信号占一位。
list域指向sigqueue链表,对于非实时信号(1-31),每个信号在链表中只能拥有一个sigqueue;对于实时信号(32-63),如果接收到多个相同的信号,每个信号都会在链表中拥有一个sigqueue。
- struct sigqueue {
- struct list_head list;
- int flags;
- siginfo_t info;
- struct user_struct *user;
- };
在每个task_struct结构中,有两个sigpending,分别是:
- struct sigpending pending;
- struct sigpending (struct signal_struct *signal)l->shared_pending;
之所以有两个sigpending,是由于Linux的线程和进程在内核中都是一个task_struct表示。所以penging域代表线程本身待处理的信号,signal->shared_pending代表线程所属进程的待处理信号。
sigqueue使用非常频繁,所以在内核中专门为其申请了kmem cache。可以在sigqueue_alloc和sigqueue_free中找到其分配、释放的实现代码。
2.信号的发送
2.1普通发送
最常用的发送信号的函数有kill, tkill和tgkill等等。其中tkill已经过时,被tgkill代替。
kill被用来给进程发消息;tgkill被用来给线程发消息。
kill
kill函数最终会调用kill_something_info:
- struct siginfo info;
- info.si_signo = sig;
- info.si_errno = 0;
- info.si_code = SI_USER;
- info.si_pid = task_tgid_vnr(current);
- info.si_uid = current->uid;
- return kill_something_info(sig, &info, pid);
- static int kill_something_info(int sig, struct siginfo *info, pid_t pid)
- // 如果pid > 0,发信号给pid指定的进程
- kill_pid_info(sig, info, find_vpid(pid));
- // 否则,如果pid != -1 && pid != 0,发信号给由-pid指定的进程组
- __kill_pgrp_info(sig, info, find_vpid(-pid);
- // 否则,如果pid == 0,发信号给自己所属的进程组
- __kill_pgrp_info(sig, info, task_pgrp(current));
- // 否则(pid == -1),发信号给除自己所属进程之外的其它所有进程
- for_each_process(p) {
- if (task_pid_vnr(p) > 1 && !same_thread_group(p, current))
- group_send_sig_info(sig, info, p);
在以上所有情况下,最终都会调用send_signal,且改函数最后一个参数为1。
tgkill
tgkill有三个参数,tgid,pid和sig,其中tgid为进程,pid为线程。tgkill最终会调用do_tkill。
- static int do_tkill(pid_t tgid, pid_t pid, int sig)
- struct siginfo info;
- info.si_signo = sig;
- info.si_errno = 0;
- info.si_code = SI_TKILL;
- info.si_pid = task_tgid_vnr(current);
- info.si_uid = current->uid;
- 。。。
- specific_send_sig_info(sig, &info, p);
- specific_send_sig_info最终会调用send_signal,且最后一个参数为0。
send_signal
有上面分析可以看出,无论是发信号给进程还是线程,最终都是调用send_signal函数,唯一区别在最后一个参数。
- <span style="font-size:12px;">int send_signal(int sig, struct siginfo *info, struct task_struct *t, int group)
-
- struct sigpending *pending;
-
- /* prepare_signal中,会调用ignore_signal判断信号是否需要被忽略,
-
- 如果是,则立即返回*/
-
- if (!prepare_signal(sig, t))
-
- return 0;
-
- // 发消息给进程和线程的区别在这里
-
- pending = group? &t->signal->shared_pending : &t->pending;
-
- // 如果是非实时信号(<32),且该信号已经在等待队列中,则忽略
-
- if (legacy_queue(pending, sig))
-
- return 0;
-
- // 注意如果传入的info指定为SEND_SIG_FORCED,那么不分配sigqueue
-
- if (info != SEND_SIG_FORCED)
-
- // 分配一个sigqueue
-
- // 将sigqueue挂入sigpending队列
-
- // 这里要注意除了SEND_SIG_FORCED之外还有几种特殊情况要考虑:
-
- // SEND_SIG_NOINFO
-
- // SEND_SIG_PRIV
-
- // 通知signalfd
-
- 。。。
-
- sigaddset(&pending->signal, sig); // 将signal加入pengding->signal位掩码中
-
- complete_signal(sig, t, group);
-
- complete_signal用于决定由哪个进程或线程处理该信号
-
- static void complete_signal(int sig, struct task_struct *p, int group)
-
- /* 如果指定的任务可以处理,则由该任务处理信号 */
-
- if (wants_signal(sig, p))
-
- t = p;
-
- /* 如果是发给指定线程的信号,或者是单线程进程,因为不满足前一个判断条件,所以直接返回,等待do_signal函数的最终处理 */
-
- else if (!group || thread_group_empty(p))
-
- return;
-
- else // 到这里,只能是发给进程的信号,且该进程是多线程的
-
- /* 从下面代码可以看出,如果是发给进程的信号,可以唤醒任意一个正在等待该信号的线程 */
-
- t = signal->curr_target;
-
- while (!wants_signal(sig, t)) {
-
- t = next_thread(t);
-
- if (t == signal->curr_target)
-
- return;
-
- }
-
- signal->curr_target = t;
-
- // 判断是否是致命信号
-
- if (sig_fatal(p, sig) &&
-
- !(signal->flags & (SIGNAL_UNKILLABLE | SIGNAL_GROUP_EXIT)) &&
-
- !sigismember(&t->real_blocked, sig) &&
-
- (sig == SIGKILL || !tracehook_consider_fatal_signal(t, sig, SIG_DFL)))
-
- if (!sig_kernel_coredump(sig))
-
- 。。。
-
- // 唤醒需要处理该信号的任务
-
- signal_wake_up(t, sig == SIGKILL);</span>
- <span style="font-size:12px;">int send_signal(int sig, struct siginfo *info, struct task_struct *t, int group)
- struct sigpending *pending;
- /* prepare_signal中,会调用ignore_signal判断信号是否需要被忽略,
- 如果是,则立即返回*/
- if (!prepare_signal(sig, t))
- return 0;
- // 发消息给进程和线程的区别在这里
- pending = group? &t->signal->shared_pending : &t->pending;
- // 如果是非实时信号(<32),且该信号已经在等待队列中,则忽略
- if (legacy_queue(pending, sig))
- return 0;
- // 注意如果传入的info指定为SEND_SIG_FORCED,那么不分配sigqueue
- if (info != SEND_SIG_FORCED)
- // 分配一个sigqueue
- // 将sigqueue挂入sigpending队列
- // 这里要注意除了SEND_SIG_FORCED之外还有几种特殊情况要考虑:
- // SEND_SIG_NOINFO
- // SEND_SIG_PRIV
- // 通知signalfd
- 。。。
- sigaddset(&pending->signal, sig); // 将signal加入pengding->signal位掩码中
- complete_signal(sig, t, group);
- complete_signal用于决定由哪个进程或线程处理该信号
- static void complete_signal(int sig, struct task_struct *p, int group)
- /* 如果指定的任务可以处理,则由该任务处理信号 */
- if (wants_signal(sig, p))
- t = p;
- /* 如果是发给指定线程的信号,或者是单线程进程,因为不满足前一个判断条件,所以直接返回,等待do_signal函数的最终处理 */
- else if (!group || thread_group_empty(p))
- return;
- else // 到这里,只能是发给进程的信号,且该进程是多线程的
- /* 从下面代码可以看出,如果是发给进程的信号,可以唤醒任意一个正在等待该信号的线程 */
- t = signal->curr_target;
- while (!wants_signal(sig, t)) {
- t = next_thread(t);
- if (t == signal->curr_target)
- return;
- }
- signal->curr_target = t;
- // 判断是否是致命信号
- if (sig_fatal(p, sig) &&
- !(signal->flags & (SIGNAL_UNKILLABLE | SIGNAL_GROUP_EXIT)) &&
- !sigismember(&t->real_blocked, sig) &&
- (sig == SIGKILL || !tracehook_consider_fatal_signal(t, sig, SIG_DFL)))
- if (!sig_kernel_coredump(sig))
- 。。。
- // 唤醒需要处理该信号的任务
- signal_wake_up(t, sig == SIGKILL);</span>
2.2强制发送
强制发送信号可以忽略信号处理的SIG_IGN标记,和stask_struct的blocked域。强制发送可由函数force_sig_specific完成,该函数最终调用force_sig_info。
- int force_sig_info(int sig, struct siginfo *info, struct task_struct *t)
- 。。。
- // 如果信号处理方式是SIG_IGN,或者task_struct的blocked域包含了该信号,
- // 则解除blocked域对该信号的屏蔽,并设置信号处理方式为SIG_DFL
- // (default处理方式)。
- action = &t->sighand->action[sig-1];
- ignored = action->sa.sa_handler == SIG_IGN;
- blocked = sigismember(&t->blocked, sig);
- if (blocked || ignored) {
- action->sa.sa_handler = SIG_DFL;
- if (blocked) {
- sigdelset(&t->blocked, sig);
- recalc_sigpending_and_wake(t);
- }
- }
- // 如果是default处理方式,清除SIGNAL_UNKILLABLE标记
- if (action->sa.sa_handler == SIG_DFL)
- t->signal->flags &= ~SIGNAL_UNKILLABLE;
- // 发信号给指定线程
- specific_send_sig_info(sig, info, t);
- 。。。
3.信号的处理
信号的处理在do_signal函数中完成。当系统即将从内核回到用户空间时,会自动调用do_signal函数,此函数与平台相关。
do_signal函数的执行过程简单分析如下:
l 判断有没有信号等待处理
l 如果有,调用do_signal_pending函数
do_signal_pending函数也是平台相关的,简单分析下执行过程:
l 调用get_signal_to_deliver得到要处理的信号。
l 调用handle_signal处理信号
这里有几点需要注意的:
l 如果是SIG_DFL或者SIG_IGN的信号,在get_signal_to_deliver函数中就已经被处理了,不会被返回,返回的都是被登记了自定义处理函数的信号。
l handle_signal并没有真正执行信号处理函数,因为信号处理函数只能在用户空间执行,不能在内核空间执行。内核空间只是设置了用户空间的堆栈,这样当do_signal返回然后切换到用户空间时,就能自动执行信号处理函数了。而执行完之后,还需要再切换到内核空间,再由内核空间重新返回到正常的用户空间程序。
下面再看看get_signal_to_deliver函数的处理过程,对signal的默认处理都在这里完成:
l 调用dequeue_signal得到待处理的信号。
n 这里隐含了信号优先级的概念,因为里面最终调用了ffz函数从signal_pending::signal域得到值最小的那个信号。
n deque_signal会先从task_struct::pending中查找signal,然后再从task_struct::signal->shared_pending中查找signal。也就是说,先处理线程信号,再处理进程信号。从这里也可以看出,多线程进程中,任何一个线程都可能处理发给进程的信号。
l 如果是SIG_IGN和SIG_DEF等情况,直接处理,并回到上一步寻找下一个待处理的信号,没找到则返回0。
l 返回待处理的信号,此信号必定是用户登记了自定义处理函数的。
3.1 sigtimedwait
如果用户任务调用了sigtimedwait,这时处理信号的过程比较特殊。此时,该用户任务会查找当前是否有自己感兴趣的信号正等待处理。
l 如果有penging的信号,则立刻返回。用户任务可以继续执行。
l 如果没有pending的信号,该任务会挂起自身,等待信号的出现。当收到信号之后,在send_signal函数的signal_wake_up中会唤醒该挂起的任务。然后该任务会调用dequeue_signal将信号取出,防止在返回用户空间时信号被do_signal再次处理。
l sigtimedwait一般与sigmask配对使用,这样,能保证我们感兴趣的信号任何时候都不会被do_signal处理。
从以上分析可知,sigtimedwait具有优先权,它能阻塞住自己感兴趣的信号,阻止do_signal函数对这些被阻塞信号的处理。
4.安装信号处理函数
信号处理函数的登记有两种,一种是老式的signal,另一种是sigaction。这两个函数最终都是调用do_sigaction。do_sigaction很简单,主要就是把k_sigaction结构放到task_struct的sighand->action中,当然还有一些小细节,比如对原先是SIG_IGN的信号的处理,等等。
注意信号处理函数是被安装到了进程,而不是线程。一个进程不管有多少线程,对指定信号都只有一个处理函数,这是因为同一进程的所有线程的task_struct结构都指向了同一个sighand结构。所以在do_sigaction中处理SIG_IGN时会有红字标注的这么一段代码:
- <span style="font-size:12px;">if (sig_handler_ignored(sig_handler(t, sig), sig)) {
-
- sigemptyset(&mask);
-
- sigaddset(&mask, sig);
-
- rm_from_queue_full(&mask, &t->signal->shared_pending);
-
- do {
-
- rm_from_queue_full(&mask, &t->pending);
-
- t = next_thread(t);
-
- } while (t != current);
-
- }
- </span>
5.其它
- <span style="font-size:12px;">if (sig_handler_ignored(sig_handler(t, sig), sig)) {
- sigemptyset(&mask);
- sigaddset(&mask, sig);
- rm_from_queue_full(&mask, &t->signal->shared_pending);
- do {
- rm_from_queue_full(&mask, &t->pending);
- t = next_thread(t);
- } while (t != current);
- }
- </span>
5.1调试信息打印
为方便调试,当收到某些信号时,Linux可以打印出当前CPU的一些信息,方便调试。
这些信号为:
SIGQUIT SIGILL SIGTRAP SIGABRT SIGFPE SIGSEGV
SIGBUS SIGSYS SIGXCPU SIGXFSZ SIGEMT_MASK
打印调试信息的开关可以在引导参数中指定:
__setup("print-fatal-signals=", setup_print_fatal_signals);
从引导参数中解析出print-fatal-signals变量后,打印开关被保存在全局变量print_fatal_signals中。
打印调试信息的函数为print_fatal_signal,里面调用了show_regs函数。
具体的调试信息打印在do_signal->get_signal_to_deliver中:
- <span style="font-size:12px;">int get_signal_to_deliver(siginfo_t *info, struct k_sigaction *return_ka,
-
- struct pt_regs *regs, void *cookie)
-
- 。。。
-
- if (sig_kernel_coredump(signr)) {
-
- if (print_fatal_signals)
-
- print_fatal_signal(regs, info->si_signo);
-
- do_coredump(info->si_signo, info->si_signo, regs);
-
- }
-
- 。。。
-
- </span>
5.2信号的通知和屏蔽
- <span style="font-size:12px;">int get_signal_to_deliver(siginfo_t *info, struct k_sigaction *return_ka,
- struct pt_regs *regs, void *cookie)
- 。。。
- if (sig_kernel_coredump(signr)) {
- if (print_fatal_signals)
- print_fatal_signal(regs, info->si_signo);
- do_coredump(info->si_signo, info->si_signo, regs);
- }
- 。。。
- </span>
Linux的信号处理中有一种机制,可以让线程或进程收到信号时通知某个回调函数,并由回调函数判断是否进入常规的信号处理。这通过task_struct结构中的这三个域完成:
- int (*notifier)(void *priv); // 收到信号
- void *notifier_data;
- sigset_t *notifier_mask;
- 在__dequeue_signal函数中,有如下代码:
- static int __dequeue_signal(struct sigpending *pending, sigset_t *mask, siginfo_t *info)
- 。。。
- if (current->notifier) {
- if (sigismember(current->notifier_mask, sig)) {
- if (!(current->notifier)(current->notifier_data)) {
- clear_thread_flag(TIF_SIGPENDING);
- return 0;
- }
- }
- }
- 。。。
即当notifier函数返回0时,屏蔽此信号。
notifier的register和unregister在如下函数中实现:
- void block_all_signals(int (*notifier)(void *priv), void *priv, sigset_t *mask);
- void unblock_all_signals(void);
从以上分析还实现函数也可以看出,一个task_struct只能有一个notifier被登记,并且没有防止多个notifier被同时注册的机制。
5.3信号处理函数的栈空间
一般情况下,信号处理函数使用的栈是线程的用户空间栈。但是,信号处理使用的栈也可以单独指定,这就是task_struct中有sas_ss_sp和sas_ss_size这两个域的原因。
信号处理专用栈可以有sys_sigaltstack指定,在handle_signal函数中,会调用get_sigframe。
如果设置了信号专用栈,get_sigframe会使用sas_ss_sp的值代替线程的用户空间栈。