|
|
|
|
# 38 | 信号(下):项目组A完成了,如何及时通知项目组B?
|
|
|
|
|
|
|
|
|
|
信号处理最常见的流程主要是两步,第一步是注册信号处理函数,第二步是发送信号和处理信号。上一节,我们讲了注册信号处理函数,那一般什么情况下会产生信号呢?我们这一节就来看一看。
|
|
|
|
|
|
|
|
|
|
## 信号的发送
|
|
|
|
|
|
|
|
|
|
有时候,我们在终端输入某些组合键的时候,会给进程发送信号,例如,Ctrl+C产生SIGINT信号,Ctrl+Z产生SIGTSTP信号。
|
|
|
|
|
|
|
|
|
|
有的时候,硬件异常也会产生信号。比如,执行了除以0的指令,CPU就会产生异常,然后把SIGFPE信号发送给进程。再如,进程访问了非法内存,内存管理模块就会产生异常,然后把信号SIGSEGV发送给进程。
|
|
|
|
|
|
|
|
|
|
这里同样是硬件产生的,对于中断和信号还是要加以区别。咱们前面讲过,中断要注册中断处理函数,但是中断处理函数是在内核驱动里面的,信号也要注册信号处理函数,信号处理函数是在用户态进程里面的。
|
|
|
|
|
|
|
|
|
|
对于硬件触发的,无论是中断,还是信号,肯定是先到内核的,然后内核对于中断和信号处理方式不同。一个是完全在内核里面处理完毕,一个是将信号放在对应的进程task\_struct里信号相关的数据结构里面,然后等待进程在用户态去处理。当然有些严重的信号,内核会把进程干掉。但是,这也能看出来,中断和信号的严重程度不一样,信号影响的往往是某一个进程,处理慢了,甚至错了,也不过这个进程被干掉,而中断影响的是整个系统。一旦中断处理中有了bug,可能整个Linux都挂了。
|
|
|
|
|
|
|
|
|
|
有时候,内核在某些情况下,也会给进程发送信号。例如,向读端已关闭的管道写数据时产生SIGPIPE信号,当子进程退出时,我们要给父进程发送SIG\_CHLD信号等。
|
|
|
|
|
|
|
|
|
|
最直接的发送信号的方法就是,通过命令kill来发送信号了。例如,我们都知道的kill -9 pid可以发送信号给一个进程,杀死它。
|
|
|
|
|
|
|
|
|
|
另外,我们还可以通过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
|
|
|
|
|
|
|
|
|
|
do\_send\_sig\_info会调用send\_signal,进而调用\_\_send\_signal。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
|
|
|
|
|
{
|
|
|
|
|
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 = from_kuid_munged(current_user_ns(), current_uid());
|
|
|
|
|
|
|
|
|
|
return kill_something_info(sig, &info, pid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static int __send_signal(int sig, struct siginfo *info, struct task_struct *t,
|
|
|
|
|
int group, int from_ancestor_ns)
|
|
|
|
|
{
|
|
|
|
|
struct sigpending *pending;
|
|
|
|
|
struct sigqueue *q;
|
|
|
|
|
int override_rlimit;
|
|
|
|
|
int ret = 0, result;
|
|
|
|
|
......
|
|
|
|
|
pending = group ? &t->signal->shared_pending : &t->pending;
|
|
|
|
|
......
|
|
|
|
|
if (legacy_queue(pending, sig))
|
|
|
|
|
goto ret;
|
|
|
|
|
|
|
|
|
|
if (sig < SIGRTMIN)
|
|
|
|
|
override_rlimit = (is_si_special(info) || info->si_code >= 0);
|
|
|
|
|
else
|
|
|
|
|
override_rlimit = 0;
|
|
|
|
|
|
|
|
|
|
q = __sigqueue_alloc(sig, t, GFP_ATOMIC | __GFP_NOTRACK_FALSE_POSITIVE,
|
|
|
|
|
override_rlimit);
|
|
|
|
|
if (q) {
|
|
|
|
|
list_add_tail(&q->list, &pending->list);
|
|
|
|
|
switch ((unsigned long) info) {
|
|
|
|
|
case (unsigned long) SEND_SIG_NOINFO:
|
|
|
|
|
q->info.si_signo = sig;
|
|
|
|
|
q->info.si_errno = 0;
|
|
|
|
|
q->info.si_code = SI_USER;
|
|
|
|
|
q->info.si_pid = task_tgid_nr_ns(current,
|
|
|
|
|
task_active_pid_ns(t));
|
|
|
|
|
q->info.si_uid = from_kuid_munged(current_user_ns(), current_uid());
|
|
|
|
|
break;
|
|
|
|
|
case (unsigned long) SEND_SIG_PRIV:
|
|
|
|
|
q->info.si_signo = sig;
|
|
|
|
|
q->info.si_errno = 0;
|
|
|
|
|
q->info.si_code = SI_KERNEL;
|
|
|
|
|
q->info.si_pid = 0;
|
|
|
|
|
q->info.si_uid = 0;
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
copy_siginfo(&q->info, info);
|
|
|
|
|
if (from_ancestor_ns)
|
|
|
|
|
q->info.si_pid = 0;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
userns_fixup_signal_uid(&q->info, t);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
......
|
|
|
|
|
out_set:
|
|
|
|
|
signalfd_notify(t, sig);
|
|
|
|
|
sigaddset(&pending->signal, sig);
|
|
|
|
|
complete_signal(sig, t, group);
|
|
|
|
|
ret:
|
|
|
|
|
return ret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
在这里,我们看到,在学习进程数据结构中task\_struct里面的sigpending。在上面的代码里面,我们先是要决定应该用哪个sigpending。这就要看我们发送的信号,是给进程的还是线程的。如果是kill发送的,也就是发送给整个进程的,就应该发送给t->signal->shared\_pending。这里面是整个进程所有线程共享的信号;如果是tkill发送的,也就是发给某个线程的,就应该发给t->pending。这里面是这个线程的task\_struct独享的。
|
|
|
|
|
|
|
|
|
|
struct sigpending里面有两个成员,一个是一个集合sigset\_t,表示都收到了哪些信号,还有一个链表,也表示收到了哪些信号。它的结构如下:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
struct sigpending {
|
|
|
|
|
struct list_head list;
|
|
|
|
|
sigset_t signal;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
如果都表示收到了信号,这两者有什么区别呢?我们接着往下看\_\_send\_signal里面的代码。接下来,我们要调用legacy\_queue。如果满足条件,那就直接退出。那legacy\_queue里面判断的是什么条件呢?我们来看它的代码。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
static inline int legacy_queue(struct sigpending *signals, int sig)
|
|
|
|
|
{
|
|
|
|
|
return (sig < SIGRTMIN) && sigismember(&signals->signal, sig);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#define SIGRTMIN 32
|
|
|
|
|
#define SIGRTMAX _NSIG
|
|
|
|
|
#define _NSIG 64
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
当信号小于SIGRTMIN,也即32的时候,如果我们发现这个信号已经在集合里面了,就直接退出了。这样会造成什么现象呢?就是信号的丢失。例如,我们发送给进程100个SIGUSR1(对应的信号为10),那最终能够被我们的信号处理函数处理的信号有多少呢?这就不好说了,比如总共5个SIGUSR1,分别是A、B、C、D、E。
|
|
|
|
|
|
|
|
|
|
如果这五个信号来得太密。A来了,但是信号处理函数还没来得及处理,B、C、D、E就都来了。根据上面的逻辑,因为A已经将SIGUSR1放在sigset\_t集合中了,因而后面四个都要丢失。 如果是另一种情况,A来了已经被信号处理函数处理了,内核在调用信号处理函数之前,我们会将集合中的标志位清除,这个时候B再来,B还是会进入集合,还是会被处理,也就不会丢。
|
|
|
|
|
|
|
|
|
|
这样信号能够处理多少,和信号处理函数什么时候被调用,信号多大频率被发送,都有关系,而且从后面的分析,我们可以知道,信号处理函数的调用时间也是不确定的。看小于32的信号如此不靠谱,我们就称它为**不可靠信号**。
|
|
|
|
|
|
|
|
|
|
如果大于32的信号是什么情况呢?我们接着看。接下来,\_\_sigqueue\_alloc会分配一个struct sigqueue对象,然后通过list\_add\_tail挂在struct sigpending里面的链表上。这样就靠谱多了是不是?如果发送过来100个信号,变成链表上的100项,都不会丢,哪怕相同的信号发送多遍,也处理多遍。因此,大于32的信号我们称为**可靠信号**。当然,队列的长度也是有限制的,如果我们执行ulimit命令,可以看到,这个限制pending signals (-i) 15408。
|
|
|
|
|
|
|
|
|
|
当信号挂到了task\_struct结构之后,最后我们需要调用complete\_signal。这里面的逻辑也很简单,就是说,既然这个进程有了一个新的信号,赶紧找一个线程处理一下吧。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
static void complete_signal(int sig, struct task_struct *p, int group)
|
|
|
|
|
{
|
|
|
|
|
struct signal_struct *signal = p->signal;
|
|
|
|
|
struct task_struct *t;
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Now find a thread we can wake up to take the signal off the queue.
|
|
|
|
|
*
|
|
|
|
|
* If the main thread wants the signal, it gets first crack.
|
|
|
|
|
* Probably the least surprising to the average bear.
|
|
|
|
|
*/
|
|
|
|
|
if (wants_signal(sig, p))
|
|
|
|
|
t = p;
|
|
|
|
|
else if (!group || thread_group_empty(p))
|
|
|
|
|
/*
|
|
|
|
|
* There is just one thread and it does not need to be woken.
|
|
|
|
|
* It will dequeue unblocked signals before it runs again.
|
|
|
|
|
*/
|
|
|
|
|
return;
|
|
|
|
|
else {
|
|
|
|
|
/*
|
|
|
|
|
* Otherwise try to find a suitable thread.
|
|
|
|
|
*/
|
|
|
|
|
t = signal->curr_target;
|
|
|
|
|
while (!wants_signal(sig, t)) {
|
|
|
|
|
t = next_thread(t);
|
|
|
|
|
if (t == signal->curr_target)
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
signal->curr_target = t;
|
|
|
|
|
}
|
|
|
|
|
......
|
|
|
|
|
/*
|
|
|
|
|
* The signal is already in the shared-pending queue.
|
|
|
|
|
* Tell the chosen thread to wake up and dequeue it.
|
|
|
|
|
*/
|
|
|
|
|
signal_wake_up(t, sig == SIGKILL);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
在找到了一个进程或者线程的task\_struct之后,我们要调用signal\_wake\_up,来企图唤醒它,signal\_wake\_up会调用signal\_wake\_up\_state。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
void signal_wake_up_state(struct task_struct *t, unsigned int state)
|
|
|
|
|
{
|
|
|
|
|
set_tsk_thread_flag(t, TIF_SIGPENDING);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!wake_up_state(t, state | TASK_INTERRUPTIBLE))
|
|
|
|
|
kick_process(t);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
signal\_wake\_up\_state里面主要做了两件事情。第一,就是给这个线程设置TIF\_SIGPENDING,这就说明其实信号的处理和进程的调度是采取这样一种类似的机制。还记得咱们调度的时候是怎么操作的吗?
|
|
|
|
|
|
|
|
|
|
当发现一个进程应该被调度的时候,我们并不直接把它赶下来,而是设置一个标识位TIF\_NEED\_RESCHED,表示等待调度,然后等待系统调用结束或者中断处理结束,从内核态返回用户态的时候,调用schedule函数进行调度。信号也是类似的,当信号来的时候,我们并不直接处理这个信号,而是设置一个标识位TIF\_SIGPENDING,来表示已经有信号等待处理。同样等待系统调用结束,或者中断处理结束,从内核态返回用户态的时候,再进行信号的处理。
|
|
|
|
|
|
|
|
|
|
signal\_wake\_up\_state的第二件事情,就是试图唤醒这个进程或者线程。wake\_up\_state会调用try\_to\_wake\_up方法。这个函数我们讲进程的时候讲过,就是将这个进程或者线程设置为TASK\_RUNNING,然后放在运行队列中,这个时候,当随着时钟不断的滴答,迟早会被调用。如果wake\_up\_state返回0,说明进程或者线程已经是TASK\_RUNNING状态了,如果它在另外一个CPU上运行,则调用kick\_process发送一个处理器间中断,强制那个进程或者线程重新调度,重新调度完毕后,会返回用户态运行。这是一个时机会检查TIF\_SIGPENDING标识位。
|
|
|
|
|
|
|
|
|
|
## 信号的处理
|
|
|
|
|
|
|
|
|
|
好了,信号已经发送到位了,什么时候真正处理它呢?
|
|
|
|
|
|
|
|
|
|
就是在从系统调用或者中断返回的时候,咱们讲调度的时候讲过,无论是从系统调用返回还是从中断返回,都会调用exit\_to\_usermode\_loop,只不过我们上次主要关注了\_TIF\_NEED\_RESCHED这个标识位,这次我们重点关注**\_TIF\_SIGPENDING标识位**。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
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进行处理。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
void do_signal(struct pt_regs *regs)
|
|
|
|
|
{
|
|
|
|
|
struct ksignal ksig;
|
|
|
|
|
|
|
|
|
|
if (get_signal(&ksig)) {
|
|
|
|
|
/* Whee! Actually deliver the signal. */
|
|
|
|
|
handle_signal(&ksig, regs);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Did we come from a system call? */
|
|
|
|
|
if (syscall_get_nr(current, regs) >= 0) {
|
|
|
|
|
/* Restart the system call - no handlers present */
|
|
|
|
|
switch (syscall_get_error(current, regs)) {
|
|
|
|
|
case -ERESTARTNOHAND:
|
|
|
|
|
case -ERESTARTSYS:
|
|
|
|
|
case -ERESTARTNOINTR:
|
|
|
|
|
regs->ax = regs->orig_ax;
|
|
|
|
|
regs->ip -= 2;
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case -ERESTART_RESTARTBLOCK:
|
|
|
|
|
regs->ax = get_nr_restart_syscall(regs);
|
|
|
|
|
regs->ip -= 2;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
restore_saved_sigmask();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
do\_signal会调用handle\_signal。按说,信号处理就是调用用户提供的信号处理函数,但是这事儿没有看起来这么简单,因为信号处理函数是在用户态的。
|
|
|
|
|
|
|
|
|
|
咱们又要来回忆系统调用的过程了。这个进程当时在用户态执行到某一行Line A,调用了一个系统调用,在进入内核的那一刻,在内核pt\_regs里面保存了用户态执行到了Line A。现在我们从系统调用返回用户态了,按说应该从pt\_regs拿出Line A,然后接着Line A执行下去,但是为了响应信号,我们不能回到用户态的时候返回Line A了,而是应该返回信号处理函数的起始地址。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
static void
|
|
|
|
|
handle_signal(struct ksignal *ksig, struct pt_regs *regs)
|
|
|
|
|
{
|
|
|
|
|
bool stepping, failed;
|
|
|
|
|
......
|
|
|
|
|
/* Are we from a system call? */
|
|
|
|
|
if (syscall_get_nr(current, regs) >= 0) {
|
|
|
|
|
/* If so, check system call restarting.. */
|
|
|
|
|
switch (syscall_get_error(current, regs)) {
|
|
|
|
|
case -ERESTART_RESTARTBLOCK:
|
|
|
|
|
case -ERESTARTNOHAND:
|
|
|
|
|
regs->ax = -EINTR;
|
|
|
|
|
break;
|
|
|
|
|
case -ERESTARTSYS:
|
|
|
|
|
if (!(ksig->ka.sa.sa_flags & SA_RESTART)) {
|
|
|
|
|
regs->ax = -EINTR;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
/* fallthrough */
|
|
|
|
|
case -ERESTARTNOINTR:
|
|
|
|
|
regs->ax = regs->orig_ax;
|
|
|
|
|
regs->ip -= 2;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
......
|
|
|
|
|
failed = (setup_rt_frame(ksig, regs) < 0);
|
|
|
|
|
......
|
|
|
|
|
signal_setup_done(failed, ksig, stepping);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
这个时候,我们就需要干预和自己来定制pt\_regs了。这个时候,我们要看,是否从系统调用中返回。如果是从系统调用返回的话,还要区分我们是从系统调用中正常返回,还是在一个非运行状态的系统调用中,因为会被信号中断而返回。
|
|
|
|
|
|
|
|
|
|
我们这里解析一个最复杂的场景。还记得咱们解析进程调度的时候,我们举的一个例子,就是从一个tap网卡中读取数据。当时我们主要关注schedule那一行,也即如果当发现没有数据的时候,就调用schedule,自己进入等待状态,然后将CPU让给其他进程。具体的代码如下:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
static ssize_t tap_do_read(struct tap_queue *q,
|
|
|
|
|
struct iov_iter *to,
|
|
|
|
|
int noblock, struct sk_buff *skb)
|
|
|
|
|
{
|
|
|
|
|
......
|
|
|
|
|
while (1) {
|
|
|
|
|
if (!noblock)
|
|
|
|
|
prepare_to_wait(sk_sleep(&q->sk), &wait,
|
|
|
|
|
TASK_INTERRUPTIBLE);
|
|
|
|
|
|
|
|
|
|
/* Read frames from the queue */
|
|
|
|
|
skb = skb_array_consume(&q->skb_array);
|
|
|
|
|
if (skb)
|
|
|
|
|
break;
|
|
|
|
|
if (noblock) {
|
|
|
|
|
ret = -EAGAIN;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (signal_pending(current)) {
|
|
|
|
|
ret = -ERESTARTSYS;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
/* Nothing to read, let's sleep */
|
|
|
|
|
schedule();
|
|
|
|
|
}
|
|
|
|
|
......
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
这里我们关注和信号相关的部分。这其实是一个信号中断系统调用的典型逻辑。
|
|
|
|
|
|
|
|
|
|
首先,我们把当前进程或者线程的状态设置为TASK\_INTERRUPTIBLE,这样才能使这个系统调用可以被中断。
|
|
|
|
|
|
|
|
|
|
其次,可以被中断的系统调用往往是比较慢的调用,并且会因为数据不就绪而通过schedule让出CPU进入等待状态。在发送信号的时候,我们除了设置这个进程和线程的\_TIF\_SIGPENDING标识位之外,还试图唤醒这个进程或者线程,也就是将它从等待状态中设置为TASK\_RUNNING。
|
|
|
|
|
|
|
|
|
|
当这个进程或者线程再次运行的时候,我们根据进程调度第一定律,从schedule函数中返回,然后再次进入while循环。由于这个进程或者线程是由信号唤醒的,而不是因为数据来了而唤醒的,因而是读不到数据的,但是在signal\_pending函数中,我们检测到了\_TIF\_SIGPENDING标识位,这说明系统调用没有真的做完,于是返回一个错误ERESTARTSYS,然后带着这个错误从系统调用返回。
|
|
|
|
|
|
|
|
|
|
然后,我们到了exit\_to\_usermode\_loop->do\_signal->handle\_signal。在这里面,当发现出现错误ERESTARTSYS的时候,我们就知道这是从一个没有调用完的系统调用返回的,设置系统调用错误码EINTR。
|
|
|
|
|
|
|
|
|
|
接下来,我们就开始折腾pt\_regs了,主要通过调用setup\_rt\_frame->\_\_setup\_rt\_frame。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
static int __setup_rt_frame(int sig, struct ksignal *ksig,
|
|
|
|
|
sigset_t *set, struct pt_regs *regs)
|
|
|
|
|
{
|
|
|
|
|
struct rt_sigframe __user *frame;
|
|
|
|
|
void __user *fp = NULL;
|
|
|
|
|
int err = 0;
|
|
|
|
|
|
|
|
|
|
frame = get_sigframe(&ksig->ka, regs, sizeof(struct rt_sigframe), &fp);
|
|
|
|
|
......
|
|
|
|
|
put_user_try {
|
|
|
|
|
......
|
|
|
|
|
/* Set up to return from userspace. If provided, use a stub
|
|
|
|
|
already in userspace. */
|
|
|
|
|
/* x86-64 should always use SA_RESTORER. */
|
|
|
|
|
if (ksig->ka.sa.sa_flags & SA_RESTORER) {
|
|
|
|
|
put_user_ex(ksig->ka.sa.sa_restorer, &frame->pretcode);
|
|
|
|
|
}
|
|
|
|
|
} put_user_catch(err);
|
|
|
|
|
|
|
|
|
|
err |= setup_sigcontext(&frame->uc.uc_mcontext, fp, regs, set->sig[0]);
|
|
|
|
|
err |= __copy_to_user(&frame->uc.uc_sigmask, set, sizeof(*set));
|
|
|
|
|
|
|
|
|
|
/* Set up registers for signal handler */
|
|
|
|
|
regs->di = sig;
|
|
|
|
|
/* In case the signal handler was declared without prototypes */
|
|
|
|
|
regs->ax = 0;
|
|
|
|
|
|
|
|
|
|
regs->si = (unsigned long)&frame->info;
|
|
|
|
|
regs->dx = (unsigned long)&frame->uc;
|
|
|
|
|
regs->ip = (unsigned long) ksig->ka.sa.sa_handler;
|
|
|
|
|
|
|
|
|
|
regs->sp = (unsigned long)frame;
|
|
|
|
|
regs->cs = __USER_CS;
|
|
|
|
|
......
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
frame的类型是rt\_sigframe。frame的意思是帧。我们只有在学习栈的时候,提到过栈帧的概念。对的,这个frame就是一个栈帧。
|
|
|
|
|
|
|
|
|
|
我们在get\_sigframe中会得到pt\_regs的sp变量,也就是原来这个程序在用户态的栈顶指针,然后get\_sigframe中,我们会将sp减去sizeof(struct rt\_sigframe),也就是把这个栈帧塞到了栈里面,然后我们又在\_\_setup\_rt\_frame中把regs->sp设置成等于frame。这就相当于强行在程序原来的用户态的栈里面插入了一个栈帧,并在最后将regs->ip设置为用户定义的信号处理函数sa\_handler。这意味着,本来返回用户态应该接着原来的代码执行的,现在不了,要执行sa\_handler了。那执行完了以后呢?按照函数栈的规则,弹出上一个栈帧来,也就是弹出了frame。
|
|
|
|
|
|
|
|
|
|
那如果我们假设sa\_handler成功返回了,怎么回到程序原来在用户态运行的地方呢?玄机就在frame里面。要想恢复原来运行的地方,首先,原来的pt\_regs不能丢,这个没问题,是在setup\_sigcontext里面,将原来的pt\_regs保存在了frame中的uc\_mcontext里面。
|
|
|
|
|
|
|
|
|
|
另外,很重要的一点,程序如何跳过去呢?在\_\_setup\_rt\_frame中,还有一个不引起重视的操作,那就是通过put\_user\_ex,将sa\_restorer放到了frame->pretcode里面,而且还是按照函数栈的规则。函数栈里面包含了函数执行完跳回去的地址。当sa\_handler执行完之后,弹出的函数栈是frame,也就应该跳到sa\_restorer的地址。这是什么地址呢?
|
|
|
|
|
|
|
|
|
|
咱们在sigaction介绍的时候就没有介绍它,在Glibc的\_\_libc\_sigaction函数中也没有注意到,它被赋值成了restore\_rt。这其实就是sa\_handler执行完毕之后,马上要执行的函数。从名字我们就能感觉到,它将恢复原来程序运行的地方。
|
|
|
|
|
|
|
|
|
|
在Glibc中,我们可以找到它的定义,它竟然调用了一个系统调用,系统调用号为\_\_NR\_rt\_sigreturn。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
RESTORE (restore_rt, __NR_rt_sigreturn)
|
|
|
|
|
|
|
|
|
|
#define RESTORE(name, syscall) RESTORE2 (name, syscall)
|
|
|
|
|
# define RESTORE2(name, syscall) \
|
|
|
|
|
asm \
|
|
|
|
|
( \
|
|
|
|
|
".LSTART_" #name ":\n" \
|
|
|
|
|
" .type __" #name ",@function\n" \
|
|
|
|
|
"__" #name ":\n" \
|
|
|
|
|
" movq $" #syscall ", %rax\n" \
|
|
|
|
|
" syscall\n" \
|
|
|
|
|
......
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
我们可以在内核里面找到\_\_NR\_rt\_sigreturn对应的系统调用。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
asmlinkage long sys_rt_sigreturn(void)
|
|
|
|
|
{
|
|
|
|
|
struct pt_regs *regs = current_pt_regs();
|
|
|
|
|
struct rt_sigframe __user *frame;
|
|
|
|
|
sigset_t set;
|
|
|
|
|
unsigned long uc_flags;
|
|
|
|
|
|
|
|
|
|
frame = (struct rt_sigframe __user *)(regs->sp - sizeof(long));
|
|
|
|
|
if (__copy_from_user(&set, &frame->uc.uc_sigmask, sizeof(set)))
|
|
|
|
|
goto badframe;
|
|
|
|
|
if (__get_user(uc_flags, &frame->uc.uc_flags))
|
|
|
|
|
goto badframe;
|
|
|
|
|
|
|
|
|
|
set_current_blocked(&set);
|
|
|
|
|
|
|
|
|
|
if (restore_sigcontext(regs, &frame->uc.uc_mcontext, uc_flags))
|
|
|
|
|
goto badframe;
|
|
|
|
|
......
|
|
|
|
|
return regs->ax;
|
|
|
|
|
......
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
在这里面,我们把上次填充的那个rt\_sigframe拿出来,然后restore\_sigcontext将pt\_regs恢复成为原来用户态的样子。从这个系统调用返回的时候,应用还误以为从上次的系统调用返回的呢。
|
|
|
|
|
|
|
|
|
|
至此,整个信号处理过程才全部结束。
|
|
|
|
|
|
|
|
|
|
## 总结时刻
|
|
|
|
|
|
|
|
|
|
信号的发送与处理是一个复杂的过程,这里来总结一下。
|
|
|
|
|
|
|
|
|
|
1. 假设我们有一个进程A,main函数里面调用系统调用进入内核。
|
|
|
|
|
2. 按照系统调用的原理,会将用户态栈的信息保存在pt\_regs里面,也即记住原来用户态是运行到了line A的地方。
|
|
|
|
|
3. 在内核中执行系统调用读取数据。
|
|
|
|
|
4. 当发现没有什么数据可读取的时候,只好进入睡眠状态,并且调用schedule让出CPU,这是进程调度第一定律。
|
|
|
|
|
5. 将进程状态设置为TASK\_INTERRUPTIBLE,可中断的睡眠状态,也即如果有信号来的话,是可以唤醒它的。
|
|
|
|
|
6. 其他的进程或者shell发送一个信号,有四个函数可以调用kill、tkill、tgkill、rt\_sigqueueinfo。
|
|
|
|
|
7. 四个发送信号的函数,在内核中最终都是调用do\_send\_sig\_info。
|
|
|
|
|
8. do\_send\_sig\_info调用send\_signal给进程A发送一个信号,其实就是找到进程A的task\_struct,或者加入信号集合,为不可靠信号,或者加入信号链表,为可靠信号。
|
|
|
|
|
9. do\_send\_sig\_info调用signal\_wake\_up唤醒进程A。
|
|
|
|
|
10. 进程A重新进入运行状态TASK\_RUNNING,根据进程调度第一定律,一定会接着schedule运行。
|
|
|
|
|
11. 进程A被唤醒后,检查是否有信号到来,如果没有,重新循环到一开始,尝试再次读取数据,如果还是没有数据,再次进入TASK\_INTERRUPTIBLE,即可中断的睡眠状态。
|
|
|
|
|
12. 当发现有信号到来的时候,就返回当前正在执行的系统调用,并返回一个错误表示系统调用被中断了。
|
|
|
|
|
13. 系统调用返回的时候,会调用exit\_to\_usermode\_loop。这是一个处理信号的时机。
|
|
|
|
|
14. 调用do\_signal开始处理信号。
|
|
|
|
|
15. 根据信号,得到信号处理函数sa\_handler,然后修改pt\_regs中的用户态栈的信息,让pt\_regs指向sa\_handler。同时修改用户态的栈,插入一个栈帧sa\_restorer,里面保存了原来的指向line A的pt\_regs,并且设置让sa\_handler运行完毕后,跳到sa\_restorer运行。
|
|
|
|
|
16. 返回用户态,由于pt\_regs已经设置为sa\_handler,则返回用户态执行sa\_handler。
|
|
|
|
|
17. sa\_handler执行完毕后,信号处理函数就执行完了,接着根据第15步对于用户态栈帧的修改,会跳到sa\_restorer运行。
|
|
|
|
|
18. sa\_restorer会调用系统调用rt\_sigreturn再次进入内核。
|
|
|
|
|
19. 在内核中,rt\_sigreturn恢复原来的pt\_regs,重新指向line A。
|
|
|
|
|
20. 从rt\_sigreturn返回用户态,还是调用exit\_to\_usermode\_loop。
|
|
|
|
|
21. 这次因为pt\_regs已经指向line A了,于是就到了进程A中,接着系统调用之后运行,当然这个系统调用返回的是它被中断了,没有执行完的错误。
|
|
|
|
|
|
|
|
|
|
![](https://static001.geekbang.org/resource/image/3d/fb/3dcb3366b11a3594b00805896b7731fb.png)
|
|
|
|
|
|
|
|
|
|
## 课堂练习
|
|
|
|
|
|
|
|
|
|
在Linux内核里面,很多地方都存在信号和信号处理,所以signal\_pending这个函数也随处可见,这样我们就能判断是否有信号发生。请你在内核代码中找到signal\_pending出现的一些地方,看有什么规律,我们后面的章节会经常遇到它。
|
|
|
|
|
|
|
|
|
|
欢迎留言和我分享你的疑惑和见解 ,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
|
|
|
|
|
|
|
|
|
|
![](https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg)
|
|
|
|
|
|