Linux系統程式設計(22)——響應訊號

尹成發表於2014-07-26


程式對訊號的響應

程式可以通過三種方式來響應一個訊號:

1、忽略訊號,即對訊號不做任何處理,其中,有兩個訊號不能忽略:SIGKILL及SIGSTOP;

2、捕捉訊號。定義訊號處理函式,當訊號發生時,執行相應的處理函式;

3、執行預設操作,Linux對每種訊號都規定了預設操作。注意,程式對實時訊號的預設反應是程式終止。

Linux究竟採用上述三種方式的哪一個來響應訊號,取決於傳遞給相應API函式的引數。

 

如果訊號的處理動作是使用者自定義函式,在訊號遞達時就呼叫這個函式,這稱為捕捉訊號。由於訊號處理函式的程式碼是在使用者空間的,處理過程比較複雜,舉例如下:

使用者程式註冊了SIGQUIT訊號的處理函式sighandler。

當前正在執行main函式,這時發生中斷或異常切換到核心態。

在中斷處理完畢後要返回使用者態的main函式之前檢查到有訊號SIGQUIT遞達。

核心決定返回使用者態後不是恢復main函式的上下文繼續執行,而是執行sighandler函式,sighandler和main函式使用不同的堆疊空間,它們之間不存在呼叫和被呼叫的關係,是兩個獨立的控制流程。

sighandler函式返回後自動執行特殊的系統呼叫sigreturn再次進入核心態。

如果沒有新的訊號要遞達,這次再返回使用者態就是恢復main函式的上下文繼續執行了。

 

 

1、 sigaction

#include <signal.h>
 
int sigaction(int signo, const structsigaction *act, struct sigaction *oact);

sigaction函式可以讀取和修改與指定訊號相關聯的處理動作。呼叫成功則返回0,出錯則返回-1。signo是指定訊號的編號。若act指標非空,則根據act修改該訊號的處理動作。若oact指標非空,則通過oact傳出該訊號原來的處理動作。act和oact指向sigaction結構體:

struct sigaction {
  void      (*sa_handler)(int);   /* addr of signal handler, */
                                      /* orSIG_IGN, or SIG_DFL */
  sigset_t sa_mask;               /*additional signals to block */
  int      sa_flags;              /* signal options, Figure 10.16*/
 
   /*alternate handler */
  void     (*sa_sigaction)(int,siginfo_t *, void *);
};


將sa_handler賦值為常數SIG_IGN傳給sigaction表示忽略訊號,賦值為常數SIG_DFL表示執行系統預設動作,賦值為一個函式指標表示用自定義函式捕捉訊號,或者說向核心註冊了一個訊號處理函式,該函式返回值為void,可以帶一個int引數,通過引數可以得知當前訊號的編號,這樣就可以用同一個函式處理多種訊號。顯然,這也是一個回撥函式,不是被main函式呼叫,而是被系統所呼叫。

 

當某個訊號的處理函式被呼叫時,核心自動將當前訊號加入程式的訊號遮蔽字,當訊號處理函式返回時自動恢復原來的訊號遮蔽字,這樣就保證了在處理某個訊號時,如果這種訊號再次產生,那麼它會被阻塞到當前處理結束為止。如果在呼叫訊號處理函式時,除了當前訊號被自動遮蔽之外,還希望自動遮蔽另外一些訊號,則用sa_mask欄位說明這些需要額外遮蔽的訊號,當訊號處理函式返回時自動恢復原來的訊號遮蔽字。

 

sa_flags欄位包含一些選項,本章的程式碼都把sa_flags設為0,sa_sigaction是實時訊號的處理函式,本章不詳細解釋這兩個欄位,有興趣的讀者參考[APUE2e]。

 

2、pause

#include <unistd.h>
 
int pause(void);

pause函式使呼叫程式掛起直到有訊號遞達。如果訊號的處理動作是終止程式,則程式終止,pause函式沒有機會返回;如果訊號的處理動作是忽略,則程式繼續處於掛起狀態,pause不返回;如果訊號的處理動作是捕捉,則呼叫了訊號處理函式之後pause返回-1,errno設定為EINTR,所以pause只有出錯的返回值(想想以前還學過什麼函式只有出錯返回值?)。錯誤碼EINTR表示“被訊號中斷”。

 

下面我們用alarm和pause實現sleep(3)函式,稱為mysleep。

#include <unistd.h>
#include <signal.h>
#include <stdio.h>
 
void sig_alrm(int signo)
{
         /*nothing to do */
}
 
unsigned int mysleep(unsigned int nsecs)
{
         structsigaction newact, oldact;
         unsignedint unslept;
 
         newact.sa_handler= sig_alrm;
         sigemptyset(&newact.sa_mask);
         newact.sa_flags= 0;
         sigaction(SIGALRM,&newact, &oldact);
 
         alarm(nsecs);
         pause();
 
         unslept= alarm(0);
         sigaction(SIGALRM,&oldact, NULL);
 
         returnunslept;
}
 
int main(void)
{
         while(1){
                   mysleep(2);
                   printf("Twoseconds passed\n");
         }
         return0;
}

1、 main函式呼叫mysleep函式,後者呼叫sigaction註冊了SIGALRM訊號的處理函式sig_alrm。

2、呼叫alarm(nsecs)設定鬧鐘。

3、呼叫pause等待,核心切換到別的程式執行。

4、nsecs秒之後,鬧鐘超時,核心發SIGALRM給這個程式。

5、從核心態返回這個程式的使用者態之前處理未決訊號,發現有SIGALRM訊號,其處理函式是sig_alrm。

6、切換到使用者態執行sig_alrm函式,進入sig_alrm函式時SIGALRM訊號被自動遮蔽,從sig_alrm函式返回時SIGALRM訊號自動解除遮蔽。然後自動執行系統呼叫sigreturn再次進入核心,再返回使用者態繼續執行程式的主控制流程(main函式呼叫的mysleep函式)。

7、pause函式返回-1,然後呼叫alarm(0)取消鬧鐘,呼叫sigaction恢復SIGALRM訊號以前的處理動作。

 

3、可重入函式與不可重入函式

 

當捕捉到訊號時,不論程式的主控制流程當前執行到哪兒,都會先跳到訊號處理函式中執行,從訊號處理函式返回後再繼續執行主控制流程。訊號處理函式是一個單獨的控制流程,因為它和主控制流程是非同步的,二者不存在呼叫和被呼叫的關係,並且使用不同的堆疊空間。引入了訊號處理函式使得一個程式具有多個控制流程,如果這些控制流程訪問相同的全域性資源(全域性變數、硬體資源等),就有可能出現衝突。

 

如果一個函式符合以下條件之一則是不可重入的:

1、呼叫了malloc或free,因為malloc也是用全域性連結串列來管理堆的。

2、呼叫了標準I/O庫函式。標準I/O庫的很多實現都以不可重入的方式使用全域性資料結構。

 

4、競態條件與sigsuspend函式

現在重新審視上面的例子“mysleep”,設想這樣的時序:

1、註冊SIGALRM訊號的處理函式。

2、呼叫alarm(nsecs)設定鬧鐘。

3、核心排程優先順序更高的程式取代當前程式執行,並且優先順序更高的程式有很多個,每個都要執行很長時間

4、nsecs秒鐘之後鬧鐘超時了,核心傳送SIGALRM訊號給這個程式,處於未決狀態。

5、優先順序更高的程式執行完了,核心要排程回這個程式執行。SIGALRM訊號遞達,執行處理函式sig_alrm之後再次進入核心。

6、返回這個程式的主控制流程,alarm(nsecs)返回,呼叫pause()掛起等待。

7、可是SIGALRM訊號已經處理完了,還等待什麼呢?

 

出現這個問題的根本原因是系統執行的時序(Timing)並不像我們寫程式時所設想的那樣。雖然alarm(nsecs)緊接著的下一行就是pause(),但是無法保證pause()一定會在呼叫alarm(nsecs)之後的nsecs秒之內被呼叫。由於非同步事件在任何時候都有可能發生(這裡的非同步事件指出現更高優先順序的程式),如果我們寫程式時考慮不周密,就可能由於時序問題而導致錯誤,這叫做競態條件(Race Condition)。

 

如何解決上述問題呢?讀者可能會想到,在呼叫pause之前遮蔽SIGALRM訊號使它不能提前遞達就可以了。看看以下方法可行嗎?

1、遮蔽SIGALRM訊號;

2、alarm(nsecs);

3、解除對SIGALRM訊號的遮蔽;

4、pause();

 

從解除訊號遮蔽到呼叫pause之間存在間隙,SIGALRM仍有可能在這個間隙遞達。要消除這個間隙,我們把解除遮蔽移到pause後面可以嗎?

1、遮蔽SIGALRM訊號;

2、alarm(nsecs);

3、pause();

4、解除對SIGALRM訊號的遮蔽;

 

這樣更不行了,還沒有解除遮蔽就呼叫pause,pause根本不可能等到SIGALRM訊號。要是“解除訊號遮蔽”和“掛起等待訊號”這兩步能合併成一個原子操作就好了,這正是sigsuspend函式的功能。sigsuspend包含了pause的掛起等待功能,同時解決了競態條件的問題,在對時序要求嚴格的場合下都應該呼叫sigsuspend而不是pause。

 

#include <signal.h>
 
int sigsuspend(const sigset_t *sigmask);

和pause一樣,sigsuspend沒有成功返回值,只有執行了一個訊號處理函式之後sigsuspend才返回,返回值為-1,errno設定為EINTR。

 

呼叫sigsuspend時,程式的訊號遮蔽字由sigmask引數指定,可以通過指定sigmask來臨時解除對某個訊號的遮蔽,然後掛起等待,當sigsuspend返回時,程式的訊號遮蔽字恢復為原來的值,如果原來對該訊號是遮蔽的,從sigsuspend返回後仍然是遮蔽的。

 

以下用sigsuspend重新實現mysleep函式:

 

unsigned int mysleep(unsigned int nsecs)
{
         structsigaction    newact, oldact;
         sigset_t            newmask, oldmask, suspmask;
         unsignedint        unslept;
 
         /*set our handler, save previous information */
         newact.sa_handler= sig_alrm;
         sigemptyset(&newact.sa_mask);
         newact.sa_flags= 0;
         sigaction(SIGALRM,&newact, &oldact);
 
         /*block SIGALRM and save current signal mask */
         sigemptyset(&newmask);
         sigaddset(&newmask,SIGALRM);
         sigprocmask(SIG_BLOCK,&newmask, &oldmask);
 
         alarm(nsecs);
 
         suspmask= oldmask;
         sigdelset(&suspmask,SIGALRM);    /* make sure SIGALRM isn'tblocked */
         sigsuspend(&suspmask);            /* wait for any signal to be caught*/
 
         /*some signal has been caught,   SIGALRM isnow blocked */
 
         unslept= alarm(0);
         sigaction(SIGALRM,&oldact, NULL);  /* reset previousaction */
 
         /*reset signal mask, which unblocks SIGALRM */
         sigprocmask(SIG_SETMASK,&oldmask, NULL);
         return(unslept);
}

 

1、如果在呼叫mysleep函式時SIGALRM訊號沒有遮蔽:

2、呼叫sigprocmask(SIG_BLOCK,&newmask, &oldmask);時遮蔽SIGALRM。

3、呼叫sigsuspend(&suspmask);時解除對SIGALRM的遮蔽,然後掛起等待待。

4、SIGALRM遞達後suspend返回,自動恢復原來的遮蔽字,也就是再次遮蔽SIGALRM。

5、呼叫sigprocmask(SIG_SETMASK,&oldmask, NULL);時再次解除對SIGALRM的遮蔽。

 

 

 

 

相關文章