程序员

Linux Signal 一网打尽

2020-09-09  本文已影响0人  扫帚的影子

Linux Signal 一网打尽

前言

Linux Signal想毕很多人都用过,比如在命令行下想要结束某个进程,我们会使用kill pid或者kill -9 pid,其实就是通过给对应的进程发送信号来完成。

Linux signal 实际上可以看作是一种进程间通讯的异步方式,进程通过对接收到的信号作相应的系统默认处理或者用户自定义处理来实现某种功能,这听起来,信号处理在行为上与中断有几分相似。

下面我们就来进入到Linux Signal的世界吧~~~

信号的使用

我们先通过一段代码实例来看一下信号量的使用吧。

void IntHandler(int signum) {
  std::cout << time(NULL) << " Got a int signal" << signum << std::endl;
  std::this_thread::sleep_for(5s);
  std::cout << time(NULL) << " Fininsh int signal" << signum << std::endl;
}

int main(int argc, char* argv[]) {
  signal(SIGINT, IntHandler);

  while(true) {
    std::this_thread::sleep_for(10s);
    std::cout << "." << std::endl;
  }

  std::cout << std::endl;

  return 0;
}

上面这段代码,我们通过signal(SIGINT, IntHandler);自定义了SIGINT信号量的处理。程序运行起来后,当按下ctrl + c时,IntHandler信号处理函数被触发。

我们再来看一个例子:

void AlarmHandler(int signum) {
  std::cout << "Got a alarm signal" << std::endl;
}

void TestAlarm() {
  struct sigaction sig;
  sig.sa_handler = AlarmHandler;
  sig.sa_flags = 0;
  sigemptyset(&sig.sa_mask);

  struct sigaction old;
  sigaction(SIGALRM, &sig, &old);
  std::cout << "A SIGALRM handler has registered" << std::endl;

  alarm(3);

  pause();

  std::cout << "Raise another alarm signal, in 2 second later" << std::endl;
  alarm(2);

  std::cout << "Star sleep 10s ..." << std::endl;
  sleep(10);

  std::cout << "Exit sleep 10s ..." << std::endl;
  std::cout << "Exit..." << std::endl;
    
  sigaction(SIGALRM, &old, NULL);
}

int main(int argc, char* argv[]) {
  TestAlarm();

  return 0;
}

上面的代码我们首先使用sigaction(SIGALRM, &sig, &old);安装了SIGALRM的用户自定义处理函数AlarmHandler, 然后我们调用alarm(3),设置了一个3s后的闹钟,再调用pause()让主程序暂停,2s后闹钟到时,AlarmHandler函数被触发,pause()调用被中断,主程序继续往下运行。

接下来又设置了一个2s的闹钟后,进入sleep(10)的10s睡眠,2s后闹钟到时,AlarmHandler函数被触发,sleep()调用被中断,主程序继续往下运行。

这里给读者留个小问题,如果把上面代码里的sleep换成std::this_thread::sleep_for(10s);,会是什么样的结果呢?

信号的发送

信号的发送,有人说那还不简单,只要知道一个进程的pid, 那就发呗~~~

之前写过一篇文章Linux PID 一网打尽, 里面介绍了在Linux系统里面,有进程,线程,线程组,进程组这几个概念。相应地,信号的发送对象也是多种多样。

信号的同步异步处理

对信号的处理可以分为同步和异步两种方式

信号的状态
信号的分类

Linux支持POSIX的标准信号和POSIX的real-time实时信号。

标准信号

标谁信号基本上是从Unix继承而来,包含下列表格中的这批信号:

截图20200901164738.png

有以下几点需要注意:

  1. SIGKILLSIGSTOP这两个信号不能被捕获,不能阻塞,也不能被忽略,完全由Linux系统自身来处理;
  2. 不支持排队,如果在某个signal被阻塞时,产生了多个这个signal, 那当这个signal变成非阻塞时,只有最先产生的那一个signal会到达并被处理;
  3. 发送到进程的多个signal, 它们到达进程并被处理的顺序是不确定的。
实时信号

从Linux2.2版本开始,支持了real-time信号,这些real-time信号量被定义在宏SIGRTMINSIGRTMAX之间,Linux系统没有给它们预先定义含义,它们可以被应用程序自由定义。

我们先看个例子:

void MinPlus2Handler(int signum) {
  std::cout << time(NULL) << " Got a SIGRTMIN + 2 signal" << signum << std::endl;
}

int main(int argc, char* argv[]) {
  signal(SIGRTMIN+2, MinPlus2Handler);

  while(true) {
    std::this_thread::sleep_for(10s);
    std::cout << "." << std::endl;
  }

  std::cout << std::endl;
    
  return 0;
}

上面代码我们通过signal(SIGRTMIN+2, MinPlus2Handler);为实时信号SIGRTMIN+2设置了handler。

在我的Linux系统上SIGRTMIN+2是36, 运行上面的程序,然后kill -36 pid,则MinPlus2Handler会被触发。

实时信号有以下这几个特点:

  1. 如果某一个实时信号当前是被阻塞的,那么如果产生了多个这个实时信号,它们会被排队,等阻塞被取消后,所有实例均可到达进程并被处理;
  2. 可以使用sigqueue来发送,同时携带用户自定义数据,这样在用户自定义的handler被回调时,可以获取到这个用户自定义数据。另外,这个 sigqueue也可以用于发送上面讲过的标准信号,但是此时针对同一个标准信号,依然不支持排队操作;
  3. 实时信号与标谁信号不同,多个实时信号到达的顺序和它们被发送的顺序是一样的。
信号的内核实现
信号的发送

我们最常用的向一个进程发送信号的方法就是kill, 它其实对应着一个系统调用也是kill,定义在kernel/signal.c中:

/**                                                  
 *  sys_kill - send a signal to a process            
 *  @pid: the PID of the process                     
 *  @sig: signal to be sent                          
 */                                                  
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)          
{                                                    
    struct kernel_siginfo info;                  
    prepare_kill_siginfo(sig, &info);            
    return kill_something_info(sig, &info, pid); 
}                                                    

此系统调用需要两个参数,在用户容间看到的进程pid(pid_t在内核代码被定义为int),另一个参数就是要发送的signal mumber。

我们接着看一下kill_something_info函数的实现,主要的逻辑都在这里:

  1. 先来看pid > 0里的处理, 前面我们讲过如果pid是大于0的,这个signal发送到这个pid相应的进程:
if (pid > 0) {                                               
▸       rcu_read_lock();                                     
▸       ret = kill_pid_info(sig, info, find_vpid(pid));      
▸       rcu_read_unlock();                                   
▸       return ret;                                          
}

我们来讲下find_vpid,在用户态调用kill时只需要知道进程的pid, 这个pid就是整数型值。kernel则需要根据这个pid

查找到其对应的struct pid, 这就是find_vpid所作的事:

find_pid_ns(nr, task_active_pid_ns(current)); 
       |                   |
       |                   |
       |                   V
       |           ns_of_pid(task_pid(current))
       |                |        |
       |                |        |
       |                |        V
       |                |  current->thread_pid (获取到current task的struct pid)
       |                |
       |                |
       |                V
       |           pid->numbers[pid->level].ns (从current task的pid中获取到 pid namespace)          |
       V
  idr_find(&ns->idr, nr) (pid_t在分配时是通过这个pid对应的pid namespace的idr来统一分配,反之查找时也是通过相应pid namespace 的idr)

我们再来看一下kill_pid_info的实现:

int kill_pid_info(int sig, struct kernel_siginfo *info, struct pid *pid)  {
    ...
        
     rcu_read_lock(); 
     //先根据pid获取到对应的task_struct
     p = pid_task(pid, PIDTYPE_PID);                                   
     if (p)                                                            
     ▸       error = group_send_sig_info(sig, info, p, PIDTYPE_TGID);  
     rcu_read_unlock();                                                
     if (likely(!p || error != -ESRCH))                                
     ▸       return error;  
} 

先根据pid获取到对应的task_struct, 然后调用group_send_sig_info -> do_send_sig_info -> send_signal->__send_signal, 我们重点来看一下最后这个函数,我把重点的流程处理提取出来,配上相应的注释:

//先放一个比较重要的struct sigpeding
struct sigpending {             
▸       struct list_head list;  
▸       sigset_t signal;        
};                              

static int __send_signal(int sig, struct kernel_siginfo *info, struct task_struct *t, 
    enum pid_type type, bool force)                               
                                     
{                                                                               
        struct sigpending *pending;                                                
        struct sigqueue *q;                                                        
        int override_rlimit;                                                       
        int ret = 0, result;                                                       
        ...
        //先根据type的不同获取到对应的sigpending 
        // 可以是线程组共享队列t->signal->shared_pending, 也可以是task私有队列t->pending
        // 当前应该是PIDTYPE_TGID,信号是发送到当前进程的,也即TGID, 使用task_struct的signal->pending来接收
        pending = (type != PIDTYPE_PID) ? &t->signal->shared_pending : &t->pending;
    
        ...
        result = TRACE_SIGNAL_ALREADY_PENDING;
        //使用legacy_queue来判断如果sig是属于上面介绍过的标谁信号,且已经在pending队列中,则不再添加,
        //这也就是上面说的标准信号不支持排队
        if (legacy_queue(pending, sig))       
            goto ret;                     
        ...
        //对于SIGKILL信号,或者是发送给内核线程的信号,不挂载到pending队列,
        //直接跳转到out_set,优先处理
        if ((sig == SIGKILL) || (t->flags & PF_KTHREAD))   
            goto out_set;                              
        ...
         //分配新的sigqueue结构体
         q = __sigqueue_alloc(sig, t, GFP_ATOMIC, override_rlimit); 
         if (q) {        
            //添加到上面获取到的pending链接里
            list_add_tail(&q->list, &pending->list); 
         }
       ...
       // 再把sig原子性地添加到pending->signal中
       sigaddset(&pending->signal, sig); 
       ...
       //挑选出合适的task来用于具体处理这个signal, 必要时唤醒这个task
       complete_signal(sig, t, type);
       ...
}

我们再来看下 complete_signal的实现:

static void complete_signal(int sig, struct task_struct *p, enum pid_type t)
{                                                                          
       struct signal_struct *signal = p->signal;                          
       struct task_struct *t;                                             
                                                                          
       //先判断当前task(主线程)是不可以处理这个signal
       if (wants_signal(sig, p))                                          
              t = p;                                                     
       else if ((type == PIDTYPE_PID) || thread_group_empty(p))           
              如果signal是发送给某个线程或者当前线程组为空,则直接返回 
              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;                                   
       }                                                                  
                                                                          
       ...                                                            
       //唤醒被中的task
       signal_wake_up(t, sig == SIGKILL);                                         
       return;                                                                    
}                                                                                  

接下来看一下一个比较重要的函数signal_wake_up

void signal_wake_up_state(struct task_struct *t, unsigned int state)            
{          
      // 设置TIF_SIGPENDING的task的flag,
      // 我们上面说过signal的处理时机是在系统调用完成后从内核态返回用户态时,
      // 此时要检查这个TIF_SIGPENDING是否被设置
      set_tsk_thread_flag(t, TIF_SIGPENDING);                                 
      /*                                                                      
       * TASK_WAKEKILL also means wake it up in the stopped/traced/killable   
       * case. We don't check t->state here because there is a race with it   
       * executing another processor and just now entering stopped state.     
       * By using wake_up_state, we ensure the process will wake up and       
       * handle its death signal.                                             
       */ 
      // 唤醒对应的task, 这部分具体实现我们留到后面讲task调度时再详细说明
      if (!wake_up_state(t, state | TASK_INTERRUPTIBLE))
          kick_process(t);                                                
}                                                                               
  1. 我们来看一下 pid = -1kill_something_info的处理,上面我们讲过此时这个signal发送到系统中除了pid=1的进程以外的其他所有当前进程有权发送signal的所有进程:

     int retval = 0, count = 0;                                  
     struct task_struct * p;                                     
                                 
     //遍历当前所有process
     for_each_process(p) {                                       
            if (task_pid_vnr(p) > 1 && !same_thread_group(p, current)) {   
                   //发送signal到每个thread group
                   int err = group_send_sig_info(sig, info, p, PIDTYPE_MAX); 
                   ++count;                                    
                   if (err != -EPERM)                          
                   ▸       retval = err;                       
            }                                                   
     }                                                           
     ret = count ? retval : -ESRCH;                              
    

    直此,信号的发送流程就介绍完了。

信号的处理

在Linux中, signal被处理的时机是在系统调用完成返回到用户态前作统一处理。

signal.png
void handle_signal(struct ksignal *ksig, struct pt_regs *regs) {
 ...
 failed = (setup_rt_frame(ksig, regs) < 0);
 signal_setup_done(failed, ksig, stepping);
}

setup_rt_frame -> __setup_rt_frame :建立新的用户空间栈,将相关参数从内核空间拷贝到用户空间。我们先看一下如果建新的用户空间栈:

void get_sigframe(struct k_sigaction *ka, struct pt_regs *regs, size_t frame_size,
  void __user **fpstate) {
...
//regs->sp就是在系统调用之初保存的当前用户态栈
unsigned long sp = regs->sp;
...
//我们在用sigaction设置signal处理handler时允许用户设置独立的用于handler的stack
if (ka->sa.sa_flags & SA_ONSTACK) {
if (sas_ss_flags(sp) == 0)
  sp = current->sas_ss_sp + current->sas_ss_size;
}
...
}

直接新的用户空间栈建立完成。

此时系统调用返回用户空间需要返回到signal设置的handler里,因此需要重置pt_regs结构,这段代码在__setup_rt_frame中:

regs->sp = (unsigned long)frame; //上面新建立的用户栈
regs->ip = (unsigned long)ksig->ka.sa.sa_handler; // ip设置成handler,这个ip最终会被加载到%RCX,供sysret使用
regs->ax = (unsigned long)sig;
regs->dx = (unsigned long)&frame->info;
regs->cx = (unsigned long)&frame->uc;

直此,万事俱备,系统调用返回到用户态的handler, 执行。

用户态的handler执行完成后,我们还需要返回内核态,这是怎么做到的呢?

在上面说过的__setup_rt_frame代码中,除了设置regs->sp为新建的用户态栈内,还会向此栈中压入调用需要的参数,和返回地址:

/* Create the ucontext.  */
unsafe_put_user(uc_flags, &frame->uc.uc_flags, Efault);
unsafe_put_user(0, &frame->uc.uc_link, Efault);
unsafe_save_altstack(&frame->uc.uc_stack, regs->sp, Efault);

/* Set up to return from userspace.  If provided, use a stub
   already in userspace.  */
unsafe_put_user(ksig->ka.sa.sa_restorer, &frame->pretcode, Efault);
unsafe_put_sigcontext(&frame->uc.uc_mcontext, fp, regs, set, Efault);
unsafe_put_sigmask(set, frame, Efault);

上面最重要的unsafe_put_user(ksig->ka.sa.sa_restorer, &frame->pretcode, Efault);, 执行完用户态的handler后,ksig->ka.sa.sa_restorer会被pop到%RIP执行,这个ka.sa.sa_restorer必须被设置,我们在调用glibc提供的sigaction函数时,blibc帮我们作了设置:

__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;
      //就是这里了
      SET_SA_RESTORER (&kact, act);
    }
  ...
}


glibc中设置restorer的代码如下:

extern void restore_rt (void) asm ("__restore_rt") attribute_hidden;
#define SET_SA_RESTORER(kact, act)                        \
  (kact)->sa_flags = (act)->sa_flags | SA_RESTORER;        \
  (kact)->sa_restorer = &restore_rt

RESTORE (restore_rt, __NR_rt_sigreturn)

到这里我们看到从用户态再次进入到内核态,其实是通过__NR_rt_sigreturn系统调用,其实现如下:

 SYSCALL_DEFINE0(rt_sigreturn)
 {
        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 (!access_ok(frame, sizeof(*frame)))
 ▸       ▸       goto badframe;
 ▸       if (__get_user(*(__u64 *)&set, (__u64 __user *)&frame->uc.uc_sigmask))
 ▸       ▸       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;

 ▸       if (restore_altstack(&frame->uc.uc_stack))
 ▸       ▸       goto badframe;
 ▸       return regs->ax;
 }

主要是恢复最初系统调用完成,为执行用户态signal handler切换stack frame之前的stack和上下文,最后返回这次系统调用的结果。

后记

直此,我们的Linux signal之旅暂告段落,还有很多signal的细节我们没有详述,这里给大家展现一下signal的来龙去脉,如有疏漏,欢迎批评指定。

上一篇 下一篇

猜你喜欢

热点阅读