进程间通信:信号

信号概述

在 Linux 操作系统中,为了响应各种各样的事件,也是定义了非常多的信号

# kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

这些信号作用:

Signal     Value     Action   Comment
──────────────────────────────────────────────────────────────────────
SIGHUP        1       Term    Hangup detected on controlling terminal
                              or death of controlling process
SIGINT        2       Term    Interrupt from keyboard
SIGQUIT       3       Core    Quit from keyboard
SIGILL        4       Core    Illegal Instruction


SIGABRT       6       Core    Abort signal from abort(3)
SIGFPE        8       Core    Floating point exception
SIGKILL       9       Term    Kill signal
SIGSEGV      11       Core    Invalid memory reference
SIGPIPE      13       Term    Broken pipe: write to pipe with no
                              readers
SIGALRM      14       Term    Timer signal from alarm(2)
SIGTERM      15       Term    Termination signal
SIGUSR1   30,10,16    Term    User-defined signal 1
SIGUSR2   31,12,17    Term    User-defined signal 2
……

用户进程对信号的处理方式:

1.执行默认操作。Linux 对每种信号都规定了默认操作,例如,上面列表中的 Term,就是终止进程的意思。Core 的意思是 Core Dump,也即终止进程后,通过 Core Dump 将当前进程的运行状态保存在文件里面,方便程序员事后进行分析问题在哪里

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

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

这个过程主要是分成两步,第一步是注册信号处理函数,第二步是发送信号

信号注册流程

int sigaction(int signum, const struct sigaction *act,
                     struct sigaction *oldact);
struct sigaction {
  __sighandler_t sa_handler;
  unsigned long sa_flags;
  __sigrestore_t sa_restorer;
  sigset_t sa_mask;    /* mask last for extensibility */
};

注意:signal 不是系统调用,而是 glibc 封装的一个函数。这样就像 man signal 里面写的一样,不同的实现方式,设置的参数会不同,会导致行为的不同

#  define signal __sysv_signal
__sighandler_t
__sysv_signal (int sig, __sighandler_t handler)
{
  struct sigaction act, oact;
......
  act.sa_handler = handler;
  __sigemptyset (&act.sa_mask);
  act.sa_flags = SA_ONESHOT | SA_NOMASK | SA_INTERRUPT;
  act.sa_flags &= ~SA_RESTART;
  if (__sigaction (sig, &act, &oact) < 0)
    return SIG_ERR;
  return oact.sa_handler;
}
weak_alias (__sysv_signal, sysv_signal)

接下来,在 glibc 中,__sigaction 会调用 __libc_sigaction,并最终调用的系统调用是 rt_sigaction

int
__sigaction (int sig, const struct sigaction *act, struct sigaction *oact)
{
......
  return __libc_sigaction (sig, act, oact);
}


int
__libc_sigaction (int sig, const struct sigaction *act, struct sigaction *oact)
{
  int result;
  struct kernel_sigaction kact, koact;


  if (act)
    {
      kact.k_sa_handler = act->sa_handler;
      memcpy (&kact.sa_mask, &act->sa_mask, sizeof (sigset_t));
      kact.sa_flags = act->sa_flags | SA_RESTORER;


      kact.sa_restorer = &restore_rt;
    }


  result = INLINE_SYSCALL (rt_sigaction, 4,
                           sig, act ? &kact : NULL,
                           oact ? &koact : NULL, _NSIG / 8);
  if (oact && result >= 0)
    {
      oact->sa_handler = koact.k_sa_handler;
      memcpy (&oact->sa_mask, &koact.sa_mask, sizeof (sigset_t));
      oact->sa_flags = koact.sa_flags;
      oact->sa_restorer = koact.sa_restorer;
    }
  return result;
}

函数调用链: __libc_sigaction -> rt_sigaction -> do_sigaction  设置 sighand 里的信号处理函数

  • 在用户程序里面,有两个函数可以调用,一个是 signal,一个是 sigaction,推荐使用 sigaction
  • 用户程序调用的是 Glibc 里面的函数,signal 调用的是 __sysv_signal,里面默认设置了一些参数,使得 signal 的功能受到了限制,sigaction 调用的是 __sigaction,参数用户可以任意设定。
  • 无论是 __sysv_signal 还是 __sigaction,调用的都是统一的一个系统调用 rt_sigaction
  • 在内核中,rt_sigaction 调用的是 do_sigaction 设置信号处理函数。在每一个进程的 task_struct 里面,都有一个 sighand 指向 struct sighand_struct,里面是一个数组,下标是信号,里面的内容是信号处理函数

截取《linux内核完全注释》

signal  sigaction
1、signal在调用handler之前先把信号的handler指针恢复;sigaction调用之后不会恢复handler指针,直到再次调用sigaction修改handler指针。
:这样,(1)signal就会丢失信号,而且不能处理重复的信号,而sigaction就可以。因为signal在得到信号和调用handler之间有个时间把handler恢复了,这样再次接收到此信号就会执行默认的handler。(虽然有些调用,在handler的以开头再次置handler,这样只能保证丢信号的概率降低,但是不能保证所有的信号都能正确处理)
2、signal在调用过程不支持信号block;sigaction调用后在handler调用之前会把屏蔽信号(屏蔽信号中自动默认包含传送的该信号)加入信号中,handler调用后会自动恢复信号到原先的值。
(2)signal处理过程中就不能提供阻塞某些信号的功能,sigaction就可以阻指定的信号和本身处理的信号,直到handler处理结束。这样就可以阻塞本身处理的信号,到handler结束就可以再次接受重复的信号。
3、sigaction提供了比signal多的多的功能,可以参考man
 

信号的发送

发送方式:

  • 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

函数分析

 1 SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
 2 {
 3   struct siginfo info;
 4 
 5   info.si_signo = sig;
 6   info.si_errno = 0;
 7   info.si_code = SI_USER;
 8   info.si_pid = task_tgid_vnr(current);
 9   info.si_uid = from_kuid_munged(current_user_ns(), current_uid());
10 
11   return kill_something_info(sig, &info, pid);
12 }
13 
14 
15 static int __send_signal(int sig, struct siginfo *info, struct task_struct *t,
16       int group, int from_ancestor_ns)
17 {
18   struct sigpending *pending;
19   struct sigqueue *q;
20   int override_rlimit;
21   int ret = 0, result;
22 ......
23   pending = group ? &t->signal->shared_pending : &t->pending;
24 ......
25   if (legacy_queue(pending, sig))
26     goto ret;
27 
28   if (sig < SIGRTMIN)
29     override_rlimit = (is_si_special(info) || info->si_code >= 0);
30   else
31     override_rlimit = 0;
32 
33   q = __sigqueue_alloc(sig, t, GFP_ATOMIC | __GFP_NOTRACK_FALSE_POSITIVE,
34     override_rlimit);
35   if (q) {
36     list_add_tail(&q->list, &pending->list);
37     switch ((unsigned long) info) {
38     case (unsigned long) SEND_SIG_NOINFO:
39       q->info.si_signo = sig;
40       q->info.si_errno = 0;
41       q->info.si_code = SI_USER;
42       q->info.si_pid = task_tgid_nr_ns(current,
43               task_active_pid_ns(t));
44       q->info.si_uid = from_kuid_munged(current_user_ns(), current_uid());
45       break;
46     case (unsigned long) SEND_SIG_PRIV:
47       q->info.si_signo = sig;
48       q->info.si_errno = 0;
49       q->info.si_code = SI_KERNEL;
50       q->info.si_pid = 0;
51       q->info.si_uid = 0;
52       break;
53     default:
54       copy_siginfo(&q->info, info);
55       if (from_ancestor_ns)
56         q->info.si_pid = 0;
57       break;
58     }
59 
60     userns_fixup_signal_uid(&q->info, t);
61 
62   } 
63 ......
64 out_set:
65   signalfd_notify(t, sig);
66   sigaddset(&pending->signal, sig);
67   complete_signal(sig, t, group);
68 ret:
69   return ret;
70 }

在上面的代码里面,我们先是要决定应该用哪个 sigpending。这就要看我们发送的信号,是给进程的还是线程的。如果是 kill 发送的,也就是发送给整个进程的,就应该发送给 t->signal->shared_pending。这里面是整个进程所有线程共享的信号;如果是 tkill 发送的,也就是发给某个线程的,就应该发给 t->pending。这里面是这个线程的 task_struct 独享的。

struct sigpending 里面有两个成员,一个是一个集合 sigset_t,表示都收到了哪些信号,还有一个链表,也表示收到了哪些信号。它的结构如下:

struct sigpending {
  struct list_head list;
  sigset_t signal;
};

当信号挂到了 task_struct 结构之后,最后我们需要调用 complete_signal。这里面的逻辑也很简单,就是说,既然这个进程有了一个新的信号,赶紧找一个线程处理一下吧

在找到了一个进程或者线程的 task_struct 之后,我们要调用 signal_wake_up,来企图唤醒它,signal_wake_up 会调用 signal_wake_up_state。

1 void signal_wake_up_state(struct task_struct *t, unsigned int state)
2 {
3   set_tsk_thread_flag(t, TIF_SIGPENDING);
4 
5 
6   if (!wake_up_state(t, state | TASK_INTERRUPTIBLE))
7     kick_process(t);
8 }

signal_wake_up_state 里面主要做了两件事情。第一,就是给这个线程设置 TIF_SIGPENDING,这就说明其实信号的处理和进程的调度是采取这样一种类似的机制

signal_wake_up_state 的第二件事情,就是试图唤醒这个进程或者线程。wake_up_state 会调用 try_to_wake_up 方法

信号的处理

无论是从系统调用返回还是从中断返回,都会调用 exit_to_usermode_loop

 1 static void exit_to_usermode_loop(struct pt_regs *regs, u32 cached_flags)
 2 {
 3   while (true) {
 4 ......
 5     if (cached_flags & _TIF_NEED_RESCHED)
 6       schedule();
 7 ......
 8     /* deal with pending signal delivery */
 9     if (cached_flags & _TIF_SIGPENDING)
10       do_signal(regs);
11 ......
12     if (!(cached_flags & EXIT_TO_USERMODE_LOOP_FLAGS))
13       break;
14   }
15 }

如果在前一个环节中,已经设置了 _TIF_SIGPENDING,我们就调用 do_signal 进行处理

 1 void do_signal(struct pt_regs *regs)
 2 {
 3   struct ksignal ksig;
 4 
 5   if (get_signal(&ksig)) {
 6     /* Whee! Actually deliver the signal.  */
 7     handle_signal(&ksig, regs);
 8     return;
 9   }
10 
11   /* Did we come from a system call? */
12   if (syscall_get_nr(current, regs) >= 0) {
13     /* Restart the system call - no handlers present */
14     switch (syscall_get_error(current, regs)) {
15     case -ERESTARTNOHAND:
16     case -ERESTARTSYS:
17     case -ERESTARTNOINTR:
18       regs->ax = regs->orig_ax;
19       regs->ip -= 2;
20       break;
21 
22     case -ERESTART_RESTARTBLOCK:
23       regs->ax = get_nr_restart_syscall(regs);
24       regs->ip -= 2;
25       break;
26     }
27   }
28   restore_saved_sigmask();
29 }

do_signal 会调用 handle_signal。按说,信号处理就是调用用户提供的信号处理函数,但是这事儿没有看起来这么简单,因为信号处理函数是在用户态的。

 1 static void
 2 handle_signal(struct ksignal *ksig, struct pt_regs *regs)
 3 {
 4   bool stepping, failed;
 5 ......
 6   /* Are we from a system call? */
 7   if (syscall_get_nr(current, regs) >= 0) {
 8     /* If so, check system call restarting.. */
 9     switch (syscall_get_error(current, regs)) {
10     case -ERESTART_RESTARTBLOCK:
11     case -ERESTARTNOHAND:
12       regs->ax = -EINTR;
13       break;
14     case -ERESTARTSYS:
15       if (!(ksig->ka.sa.sa_flags & SA_RESTART)) {
16         regs->ax = -EINTR;
17         break;
18       }
19     /* fallthrough */
20     case -ERESTARTNOINTR:
21       regs->ax = regs->orig_ax;
22       regs->ip -= 2;
23       break;
24     }
25   }
26 ......
27   failed = (setup_rt_frame(ksig, regs) < 0);
28 ......
29   signal_setup_done(failed, ksig, stepping);
30 }

我们就需要干预和自己来定制 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. 当发现有信号到来的时候,就返回当前正在执行的系统调用,并返回一个错误表示系统调用被中断了。系统调用返回的时候,会调用 exit_to_usermode_loop。这是一个处理信号的时机。
  13. 调用 do_signal 开始处理信号。根据信号,得到信号处理函数 sa_handler,然后修改 pt_regs 中的用户态栈的信息,让 pt_regs 指向 sa_handler。
  14. 同时修改用户态的栈,插入一个栈帧 sa_restorer,里面保存了原来的指向 line A 的 pt_regs,并且设置让 sa_handler 运行完毕后,跳到 sa_restorer 运行。
  15. 返回用户态,由于 pt_regs 已经设置为 sa_handler,则返回用户态执行 sa_handler。
  16. sa_handler 执行完毕后,信号处理函数就执行完了,接着根据第 15 步对于用户态栈帧的修改,会跳到 sa_restorer 运行。
  17. sa_restorer 会调用系统调用 rt_sigreturn 再次进入内核。在内核中,rt_sigreturn 恢复原来的 pt_regs,重新指向 line A。
  18. 从 rt_sigreturn 返回用户态,还是调用 exit_to_usermode_loop。这次因为 pt_regs 已经指向 line A 了,于是就到了进程 A 中,接着系统调用之后运行,当然这个系统调用返回的是它被中断了,没有执行完的错误。
原文地址:https://www.cnblogs.com/mysky007/p/12335905.html