Linux Signal 一網(wǎng)打盡

Linux Signal 一網(wǎng)打盡

前言

Linux Signal想畢很多人都用過步做,比如在命令行下想要結(jié)束某個進(jìn)程法焰,我們會使用kill pid或者kill -9 pid缰雇,其實(shí)就是通過給對應(yīng)的進(jìn)程發(fā)送信號來完成。

Linux signal 實(shí)際上可以看作是一種進(jìn)程間通訊的異步方式荆姆,進(jìn)程通過對接收到的信號作相應(yīng)的系統(tǒng)默認(rèn)處理或者用戶自定義處理來實(shí)現(xiàn)某種功能蔼囊,這聽起來焚志,信號處理在行為上與中斷有幾分相似。

下面我們就來進(jìn)入到Linux Signal的世界吧~~~

信號的使用

我們先通過一段代碼實(shí)例來看一下信號量的使用吧畏鼓。

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信號量的處理。程序運(yùn)行起來后云矫,當(dāng)按下ctrl + c時膳沽,IntHandler信號處理函數(shù)被觸發(fā)。

我們再來看一個例子:

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的用戶自定義處理函數(shù)AlarmHandler, 然后我們調(diào)用alarm(3)让禀,設(shè)置了一個3s后的鬧鐘挑社,再調(diào)用pause()讓主程序暫停,2s后鬧鐘到時巡揍,AlarmHandler函數(shù)被觸發(fā)痛阻,pause()調(diào)用被中斷,主程序繼續(xù)往下運(yùn)行腮敌。

接下來又設(shè)置了一個2s的鬧鐘后阱当,進(jìn)入sleep(10)的10s睡眠,2s后鬧鐘到時糜工,AlarmHandler函數(shù)被觸發(fā)弊添,sleep()調(diào)用被中斷,主程序繼續(xù)往下運(yùn)行捌木。

這里給讀者留個小問題油坝,如果把上面代碼里的sleep換成std::this_thread::sleep_for(10s);,會是什么樣的結(jié)果呢?

信號的發(fā)送

信號的發(fā)送免钻,有人說那還不簡單,只要知道一個進(jìn)程的pid, 那就發(fā)唄~~~

之前寫過一篇文章Linux PID 一網(wǎng)打盡, 里面介紹了在Linux系統(tǒng)里面崔拥,有進(jìn)程极舔,線程,線程組链瓦,進(jìn)程組這幾個概念拆魏。相應(yīng)地,信號的發(fā)送對象也是多種多樣慈俯。

  • raise: 發(fā)送signal到當(dāng)前的調(diào)用進(jìn)程或線程渤刃。如果是單線程程序,就是發(fā)送給這個進(jìn)程贴膘,也就是發(fā)送到主線程卖子;對于多線程程序,發(fā)送給當(dāng)前調(diào)用raise的這個線程刑峡;
  • kill : 發(fā)送signal到任意的進(jìn)程組或進(jìn)程洋闽,它需要一個pid參數(shù)。
    1. 如果pid是大于0的突梦,這個signal發(fā)送到這個pid相應(yīng)的進(jìn)程诫舅,一個進(jìn)程里可能有多個線程,這個signal可以被其中任意一個線程接收并處理(前提是這個線程沒有屏蔽掉這個信號)宫患;
    2. 如果pid等于0刊懈,這個signal發(fā)送到當(dāng)前調(diào)用這個kill的進(jìn)程所在的進(jìn)程組里的每一個進(jìn)程;
    3. 如果pid等于-1娃闲,這個signal發(fā)送到系統(tǒng)中除了pid=1的進(jìn)程以外的其他所有當(dāng)前進(jìn)程有權(quán)發(fā)送signal的所有進(jìn)程虚汛;
    4. 如果pid小于-1龙巨, 這個signal發(fā)送到進(jìn)程組ID為-pid的進(jìn)程組中的每一個進(jìn)程每强。
  • tgkill : 可以直接將signal發(fā)送到一個線程組里的某一個線程奶赠。
信號的同步異步處理

對信號的處理可以分為同步和異步兩種方式

  • 異步方式圃郊,是大家最熟悉也是最常用的宗收,就像我們上面的例子一樣港庄,使用signalsigaction設(shè)置處理信號的handler欢搜;
  • 同步方式牛曹,大家用得少捌年,顧名思義就是在代碼里阻塞住瓢娜,直接有signal到來,然后在這一點(diǎn)kernel將到來的signal的相關(guān)信息返回給程序礼预,一般有兩種方法:
    1. 調(diào)用sigwaitinfo, sigtimedwaitsigwait: 阻塞住代碼運(yùn)行眠砾,直接有signal到來;
    2. signalfd: 這完全貫徹了Linux下一切皆文件的方針,通過這個接口你可以將對signal的接收抽象成通過的對這個fd的讀取操作托酸,因?yàn)橐呀?jīng)是fd了褒颈,它就是變通的文件的fd沒什么異同了柒巫,你可以通過read阻塞到來,也可以使用select,epoll等來多路復(fù)用谷丸。
信號的狀態(tài)
  • pending狀態(tài):從信號產(chǎn)生到信號到達(dá)進(jìn)程這段時間是處于pending狀態(tài)堡掏,所有的信號都在是系統(tǒng)調(diào)用完成返回用戶空間前被處理掉,如果當(dāng)前的進(jìn)程沒有觸發(fā)任可的系統(tǒng)調(diào)用刨疼,那到達(dá)它的所有信號都不會被處理泉唁;
  • blocked狀態(tài):可以理解成信號被阻塞,它不能到達(dá)當(dāng)前進(jìn)程揩慕,直到當(dāng)前進(jìn)程解除了對此signal的阻塞亭畜,那之前已經(jīng)被阻塞的signal又會到來被處理。進(jìn)程中的每一個線程都有自己獨(dú)立的signal mask, 這個signal mask可以被用于設(shè)置當(dāng)前線程要阻塞哪些signal迎卤。
信號的分類

Linux支持POSIX的標(biāo)準(zhǔn)信號和POSIX的real-time實(shí)時信號拴鸵。

標(biāo)準(zhǔn)信號

標(biāo)誰信號基本上是從Unix繼承而來,包含下列表格中的這批信號:

截圖20200901164738.png

有以下幾點(diǎn)需要注意:

  1. SIGKILLSIGSTOP這兩個信號不能被捕獲蜗搔,不能阻塞宝踪,也不能被忽略,完全由Linux系統(tǒng)自身來處理碍扔;
  2. 不支持排隊(duì)瘩燥,如果在某個signal被阻塞時,產(chǎn)生了多個這個signal, 那當(dāng)這個signal變成非阻塞時不同,只有最先產(chǎn)生的那一個signal會到達(dá)并被處理厉膀;
  3. 發(fā)送到進(jìn)程的多個signal, 它們到達(dá)進(jìn)程并被處理的順序是不確定的。
實(shí)時信號

從Linux2.2版本開始二拐,支持了real-time信號服鹅,這些real-time信號量被定義在宏SIGRTMINSIGRTMAX之間,Linux系統(tǒng)沒有給它們預(yù)先定義含義百新,它們可以被應(yīng)用程序自由定義企软。

我們先看個例子:

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);為實(shí)時信號SIGRTMIN+2設(shè)置了handler。

在我的Linux系統(tǒng)上SIGRTMIN+2是36, 運(yùn)行上面的程序饭望,然后kill -36 pid仗哨,則MinPlus2Handler會被觸發(fā)。

實(shí)時信號有以下這幾個特點(diǎn):

  1. 如果某一個實(shí)時信號當(dāng)前是被阻塞的铅辞,那么如果產(chǎn)生了多個這個實(shí)時信號厌漂,它們會被排隊(duì),等阻塞被取消后斟珊,所有實(shí)例均可到達(dá)進(jìn)程并被處理苇倡;
  2. 可以使用sigqueue來發(fā)送,同時攜帶用戶自定義數(shù)據(jù),這樣在用戶自定義的handler被回調(diào)時旨椒,可以獲取到這個用戶自定義數(shù)據(jù)晓褪。另外,這個 sigqueue也可以用于發(fā)送上面講過的標(biāo)準(zhǔn)信號综慎,但是此時針對同一個標(biāo)準(zhǔn)信號涣仿,依然不支持排隊(duì)操作;
  3. 實(shí)時信號與標(biāo)誰信號不同寥粹,多個實(shí)時信號到達(dá)的順序和它們被發(fā)送的順序是一樣的变过。
信號的內(nèi)核實(shí)現(xiàn)
信號的發(fā)送

我們最常用的向一個進(jìn)程發(fā)送信號的方法就是kill, 它其實(shí)對應(yīng)著一個系統(tǒng)調(diào)用也是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); 
}                                                    

此系統(tǒng)調(diào)用需要兩個參數(shù)埃元,在用戶容間看到的進(jìn)程pid(pid_t在內(nèi)核代碼被定義為int)涝涤,另一個參數(shù)就是要發(fā)送的signal mumber。

我們接著看一下kill_something_info函數(shù)的實(shí)現(xiàn)岛杀,主要的邏輯都在這里:

  1. 先來看pid > 0里的處理, 前面我們講過如果pid是大于0的阔拳,這個signal發(fā)送到這個pid相應(yīng)的進(jìn)程:
if (pid > 0) {                                               
?       rcu_read_lock();                                     
?       ret = kill_pid_info(sig, info, find_vpid(pid));      
?       rcu_read_unlock();                                   
?       return ret;                                          
}

我們來講下find_vpid,在用戶態(tài)調(diào)用kill時只需要知道進(jìn)程的pid, 這個pid就是整數(shù)型值类嗤。kernel則需要根據(jù)這個pid

查找到其對應(yīng)的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對應(yīng)的pid namespace的idr來統(tǒng)一分配糊肠,反之查找時也是通過相應(yīng)pid namespace 的idr)

我們再來看一下kill_pid_info的實(shí)現(xiàn):

int kill_pid_info(int sig, struct kernel_siginfo *info, struct pid *pid)  {
    ...
        
     rcu_read_lock(); 
     //先根據(jù)pid獲取到對應(yīng)的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;  
} 

先根據(jù)pid獲取到對應(yīng)的task_struct, 然后調(diào)用group_send_sig_info -> do_send_sig_info -> send_signal->__send_signal, 我們重點(diǎn)來看一下最后這個函數(shù),我把重點(diǎn)的流程處理提取出來遗锣,配上相應(yīng)的注釋:

//先放一個比較重要的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;                                                       
        ...
        //先根據(jù)type的不同獲取到對應(yīng)的sigpending 
        // 可以是線程組共享隊(duì)列t->signal->shared_pending, 也可以是task私有隊(duì)列t->pending
        // 當(dāng)前應(yīng)該是PIDTYPE_TGID货裹,信號是發(fā)送到當(dāng)前進(jìn)程的,也即TGID, 使用task_struct的signal->pending來接收
        pending = (type != PIDTYPE_PID) ? &t->signal->shared_pending : &t->pending;
    
        ...
        result = TRACE_SIGNAL_ALREADY_PENDING;
        //使用legacy_queue來判斷如果sig是屬于上面介紹過的標(biāo)誰信號精偿,且已經(jīng)在pending隊(duì)列中弧圆,則不再添加,
        //這也就是上面說的標(biāo)準(zhǔn)信號不支持排隊(duì)
        if (legacy_queue(pending, sig))       
            goto ret;                     
        ...
        //對于SIGKILL信號笔咽,或者是發(fā)送給內(nèi)核線程的信號搔预,不掛載到pending隊(duì)列,
        //直接跳轉(zhuǎn)到out_set,優(yōu)先處理
        if ((sig == SIGKILL) || (t->flags & PF_KTHREAD))   
            goto out_set;                              
        ...
         //分配新的sigqueue結(jié)構(gòu)體
         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的實(shí)現(xiàn):

static void complete_signal(int sig, struct task_struct *p, enum pid_type t)
{                                                                          
       struct signal_struct *signal = p->signal;                          
       struct task_struct *t;                                             
                                                                          
       //先判斷當(dāng)前task(主線程)是不可以處理這個signal
       if (wants_signal(sig, p))                                          
              t = p;                                                     
       else if ((type == PIDTYPE_PID) || thread_group_empty(p))           
              如果signal是發(fā)送給某個線程或者當(dāng)前線程組為空叶组,則直接返回 
              return;                                                    
       else {                                                             
              //遍歷當(dāng)前線程組所有線程拯田,找到合適的線程
              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;                                                                    
}                                                                                  

接下來看一下一個比較重要的函數(shù)signal_wake_up

void signal_wake_up_state(struct task_struct *t, unsigned int state)            
{          
      // 設(shè)置TIF_SIGPENDING的task的flag,
      // 我們上面說過signal的處理時機(jī)是在系統(tǒng)調(diào)用完成后從內(nèi)核態(tài)返回用戶態(tài)時,
      // 此時要檢查這個TIF_SIGPENDING是否被設(shè)置
      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.                                             
       */ 
      // 喚醒對應(yīng)的task, 這部分具體實(shí)現(xiàn)我們留到后面講task調(diào)度時再詳細(xì)說明
      if (!wake_up_state(t, state | TASK_INTERRUPTIBLE))
          kick_process(t);                                                
}                                                                               
  1. 我們來看一下 pid = -1kill_something_info的處理甩十,上面我們講過此時這個signal發(fā)送到系統(tǒng)中除了pid=1的進(jìn)程以外的其他所有當(dāng)前進(jìn)程有權(quán)發(fā)送signal的所有進(jìn)程:

     int retval = 0, count = 0;                                  
     struct task_struct * p;                                     
                                 
     //遍歷當(dāng)前所有process
     for_each_process(p) {                                       
            if (task_pid_vnr(p) > 1 && !same_thread_group(p, current)) {   
                   //發(fā)送signal到每個thread group
                   int err = group_send_sig_info(sig, info, p, PIDTYPE_MAX); 
                   ++count;                                    
                   if (err != -EPERM)                          
                   ?       retval = err;                       
            }                                                   
     }                                                           
     ret = count ? retval : -ESRCH;                              
    

    直此船庇,信號的發(fā)送流程就介紹完了。

信號的處理

在Linux中侣监, signal被處理的時機(jī)是在系統(tǒng)調(diào)用完成返回到用戶態(tài)前作統(tǒng)一處理溢十。

  • 我們先來簡單回顧下系統(tǒng)調(diào)用相關(guān)的知識點(diǎn)

    簡單來說,傳統(tǒng)系統(tǒng)調(diào)用使用int 0x80中斷陷入內(nèi)核达吞,走的是中斷處理的流程张弛,需要作權(quán)限驗(yàn)證,內(nèi)核棧切換,segment的加載等等吞鸭,速度慢寺董。進(jìn)入到64位后,CPU提供了新的syscall指令使用MSR寄存器來加載code segment和系統(tǒng)調(diào)用處理入口刻剥,這被稱名快速系統(tǒng)調(diào)用遮咖,在kernel啟動時被初始化(arch/x86/kernel/cpu/common.c):

     void syscall_init(void)                                          
     {                                                                
     ?       wrmsr(MSR_STAR, 0, (__USER32_CS << 16) | __KERNEL_CS);   
     ?       wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);      
         
             /* Flags to clear on syscall */                      
            wrmsrl(MSR_SYSCALL_MASK,                             
                   X86_EFLAGS_TF|X86_EFLAGS_DF|X86_EFLAGS_IF|    
                   X86_EFLAGS_IOPL|X86_EFLAGS_AC|X86_EFLAGS_NT); 
    }
    

    可以看到當(dāng)前的系統(tǒng)調(diào)用處理入口是entry_SYSCALL_64,這是段匯編造虏,位于arch/x86/entry/entry_64.S中御吞,我們簡單過下這段匯編:

    ENTRY(entry_SYSCALL_64) 
       //交換用戶態(tài)和內(nèi)核態(tài)gs寄存器,
       //交換內(nèi)核棧和用戶態(tài)棧漓藕,
       //交換CR3,頁表
       //這段執(zhí)行完后,%rsp寄存器指向內(nèi)核棧
       swapgs                                              
       /* tss.sp2 is scratch space. */                     
       movq?   %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)     
       SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp               
       movq?   PER_CPU_VAR(cpu_current_top_of_stack), %rsp 
       
        //在內(nèi)核棧上構(gòu)造pt_regs,保存用戶CS陶珠,DS,用戶態(tài)棧到內(nèi)核棧
        pushq?  $__USER_DS?     ?       ?       ?       /* pt_regs->ss */     
        pushq?  PER_CPU_VAR(cpu_tss_rw + TSS_sp2)?      /* pt_regs->sp */     
        pushq?  %r11?   ?       ?       ?       ?       /* pt_regs->flags */  
        pushq?  $__USER_CS?     ?       ?       ?       /* pt_regs->cs */     
        pushq?  %rcx?   ?       ?       ?       ?       /* pt_regs->ip */     
        ntry_SYSCALL_64_after_hwframe)                                        
        pushq?  %rax?   ?       ?       ?       ?       /* pt_regs->orig_ax */
        
        // 具體的中斷處理
        call?   do_syscall_64?
        ....
        //后面就是前面操作的逆操作
        //從pt_regs里恢復(fù)%rcx,%r11
        movq?   RCX(%rsp), %rcx
        movq?   RIP(%rsp), %r11
        ....
        USERGS_SYSRET64
     END(entry_SYSCALL_64)
    

    上面有一句很重要pushq? %rcx /* pt_regs->ip, %rcx 寄存器保存著系統(tǒng)調(diào)用完成后返回到用戶空間的地址享钞,這個地址被保存到了當(dāng)前的內(nèi)核棧上揍诽,即pt_regs->ip

    最后的USERGS_SYSRET64返回用戶空間栗竖,它的實(shí)現(xiàn)是:

    swapgs;
    sysretq;
    

    sysretq指令從RCX寄存器加載下一條指令地址到RIP寄存器暑脆,CPU繼續(xù)執(zhí)行,這樣就返回了用空間狐肢。

    上面這段內(nèi)容雖簡單添吗,但很重要,我們后面會用到份名。

    往下執(zhí)行的調(diào)用路徑是do_syscall_64 -> syscall_return_slowpath -> exit_to_usermode_loop, 我們重點(diǎn)來看exit_to_usermode_loop:

    //這里是一個調(diào)度點(diǎn)
    if (cached_flags & _TIF_NEED_RESCHED)   
      schedule();                      
    ...                               
    /* deal with pending signal delivery */ 
    if (cached_flags & _TIF_SIGPENDING)     
      do_signal(regs);                
    
  • signal的處理碟联,主要在do_signal中,我們只保留其主要流程:

    void do_signal(struct pt_regs *regs)                                                   
    {                                                                                      
           struct ksignal ksig;                                                                  
           ...                 
                                                                                          
           if (get_signal(&ksig)) {                                                       
                  ...                                                            
                  handle_signal(&ksig, regs);                                            
                  return;                                                                
           }                                                                            
           ....                                                                    
           /* If there's no signal to deliver, restore the saved sigmask back */          
           restore_saved_sigmask();                                                       
    } 
    

    通過上面的講解我們知道signal被處理的時機(jī)是在系統(tǒng)調(diào)用處理完后在返回到用戶態(tài)之前同窘,此時還是處于內(nèi)核態(tài)玄帕。如果針對待處理的signal我們在應(yīng)用程序中設(shè)置了用戶自定義的handler,此時需要在內(nèi)核態(tài)調(diào)用這個handler, 但是handler是用戶態(tài)代碼想邦,內(nèi)核態(tài)不能直接調(diào)用裤纹,這里需要在用戶態(tài)創(chuàng)建臨時棧幀來運(yùn)行這個handler, 運(yùn)行完成后再返回到內(nèi)核態(tài),然后再次返回到用戶態(tài)丧没,完成整個的系統(tǒng)調(diào)用流程鹰椒。

    網(wǎng)上有張經(jīng)典圖,我們借用一下呕童,來說明這個過程:

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 :建立新的用戶空間棧漆际,將相關(guān)參數(shù)從內(nèi)核空間拷貝到用戶空間。我們先看一下如果建新的用戶空間棧:

void get_sigframe(struct k_sigaction *ka, struct pt_regs *regs, size_t frame_size,
  void __user **fpstate) {
...
//regs->sp就是在系統(tǒng)調(diào)用之初保存的當(dāng)前用戶態(tài)棧
unsigned long sp = regs->sp;
...
//我們在用sigaction設(shè)置signal處理handler時允許用戶設(shè)置獨(dú)立的用于handler的stack
if (ka->sa.sa_flags & SA_ONSTACK) {
if (sas_ss_flags(sp) == 0)
  sp = current->sas_ss_sp + current->sas_ss_size;
}
...
}

直接新的用戶空間棧建立完成夺饲。

此時系統(tǒng)調(diào)用返回用戶空間需要返回到signal設(shè)置的handler里奸汇,因此需要重置pt_regs結(jié)構(gòu)施符,這段代碼在__setup_rt_frame中:

regs->sp = (unsigned long)frame; //上面新建立的用戶棧
regs->ip = (unsigned long)ksig->ka.sa.sa_handler; // ip設(shè)置成handler,這個ip最終會被加載到%RCX擂找,供sysret使用
regs->ax = (unsigned long)sig;
regs->dx = (unsigned long)&frame->info;
regs->cx = (unsigned long)&frame->uc;

直此戳吝,萬事俱備,系統(tǒng)調(diào)用返回到用戶態(tài)的handler, 執(zhí)行贯涎。

用戶態(tài)的handler執(zhí)行完成后听哭,我們還需要返回內(nèi)核態(tài),這是怎么做到的呢塘雳?

在上面說過的__setup_rt_frame代碼中陆盘,除了設(shè)置regs->sp為新建的用戶態(tài)棧內(nèi),還會向此棧中壓入調(diào)用需要的參數(shù)败明,和返回地址:

/* 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);, 執(zhí)行完用戶態(tài)的handler后隘马,ksig->ka.sa.sa_restorer會被pop到%RIP執(zhí)行,這個ka.sa.sa_restorer必須被設(shè)置肩刃,我們在調(diào)用glibc提供的sigaction函數(shù)時祟霍,blibc幫我們作了設(shè)置:

__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中設(shè)置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)

到這里我們看到從用戶態(tài)再次進(jìn)入到內(nèi)核態(tài)杏头,其實(shí)是通過__NR_rt_sigreturn系統(tǒng)調(diào)用盈包,其實(shí)現(xiàn)如下:

 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;
 }

主要是恢復(fù)最初系統(tǒng)調(diào)用完成,為執(zhí)行用戶態(tài)signal handler切換stack frame之前的stack和上下文醇王,最后返回這次系統(tǒng)調(diào)用的結(jié)果呢燥。

后記

直此,我們的Linux signal之旅暫告段落寓娩,還有很多signal的細(xì)節(jié)我們沒有詳述叛氨,這里給大家展現(xiàn)一下signal的來龍去脈,如有疏漏棘伴,歡迎批評指定寞埠。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市焊夸,隨后出現(xiàn)的幾起案子仁连,更是在濱河造成了極大的恐慌,老刑警劉巖阱穗,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件饭冬,死亡現(xiàn)場離奇詭異,居然都是意外死亡揪阶,警方通過查閱死者的電腦和手機(jī)昌抠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鲁僚,“玉大人炊苫,你說我怎么就攤上這事裁厅。” “怎么了侨艾?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵姐直,是天一觀的道長。 經(jīng)常有香客問我蒋畜,道長声畏,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任姻成,我火速辦了婚禮插龄,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘科展。我一直安慰自己均牢,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布才睹。 她就那樣靜靜地躺著徘跪,像睡著了一般。 火紅的嫁衣襯著肌膚如雪琅攘。 梳的紋絲不亂的頭發(fā)上垮庐,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天,我揣著相機(jī)與錄音坞琴,去河邊找鬼哨查。 笑死,一個胖子當(dāng)著我的面吹牛剧辐,可吹牛的內(nèi)容都是我干的寒亥。 我是一名探鬼主播,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼荧关,長吁一口氣:“原來是場噩夢啊……” “哼溉奕!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起忍啤,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤加勤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后檀轨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體胸竞,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年参萄,在試婚紗的時候發(fā)現(xiàn)自己被綠了卫枝。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡讹挎,死狀恐怖校赤,靈堂內(nèi)的尸體忽然破棺而出吆玖,到底是詐尸還是另有隱情,我是刑警寧澤马篮,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布沾乘,位于F島的核電站,受9級特大地震影響浑测,放射性物質(zhì)發(fā)生泄漏翅阵。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一迁央、第九天 我趴在偏房一處隱蔽的房頂上張望掷匠。 院中可真熱鬧,春花似錦岖圈、人聲如沸讹语。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽顽决。三九已至,卻和暖如春导匣,著一層夾襖步出監(jiān)牢的瞬間才菠,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工逐抑, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鸠儿,地道東北人屹蚊。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓厕氨,卻偏偏與公主長得像,于是被迫代替她去往敵國和親汹粤。 傳聞我的和親對象是個殘疾皇子命斧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評論 2 353