Linux系統程式設計(22)——響應訊號
程式對訊號的響應
程式可以通過三種方式來響應一個訊號:
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的遮蔽。
相關文章
- Linux系統程式設計—訊號捕捉Linux程式設計
- Linux系統程式設計:訊號捕捉Linux程式設計
- 【Linux系統程式設計】Linux訊號列表Linux程式設計
- linux系統程式設計之訊號(一):中斷與訊號Linux程式設計
- 【linux】系統程式設計-1-程式、管道和訊號Linux程式設計
- 響應式程式設計與響應式系統程式設計
- Linux系統程式設計(20)——訊號基本概念Linux程式設計
- Linux系統程式設計(21)——訊號的產生Linux程式設計
- Linux系統程式設計(24)——訊號的生命週期Linux程式設計
- linux系統程式設計之訊號(五):訊號集操作函式,訊號阻塞與未決Linux程式設計函式
- Linux系統程式設計之訊號中斷處理(下)Linux程式設計
- Linux系統程式設計之訊號中斷處理(上)Linux程式設計
- linux系統程式設計之訊號(三):訊號安裝、signal、kill,arise講解Linux程式設計
- linux系統程式設計之訊號(七):被訊號中斷的系統呼叫和庫函式處理方式Linux程式設計函式
- linux系統程式設計之訊號(四):alarm和可重入函式Linux程式設計函式
- linux系統程式設計之訊號(二):訊號處理流程(產生、註冊、登出、執行)Linux程式設計
- 【Linux網路程式設計-1】訊號Linux程式設計
- Linux系統程式設計之程式間通訊方式:訊息佇列Linux程式設計佇列
- 【linux】系統程式設計-6-POSIX標準下的訊號量與互斥鎖Linux程式設計
- 【linux】系統程式設計-2-訊息佇列Linux程式設計佇列
- 系統程式設計-訊號-總體概述和signal基本使用程式設計
- 系統程式設計——管道通訊程式設計
- Linux系統程式設計之程式間通訊方式:管道(二)Linux程式設計
- Linux系統程式設計之程式間通訊方式:管道(一)Linux程式設計
- Linux系統程式設計(11)——程式間通訊之有名管道Linux程式設計
- Linux系統程式設計之程式間通訊方式:命名管道(二)Linux程式設計
- Linux系統程式設計之程式間通訊方式:命名管道(一)Linux程式設計
- Linux系統程式設計【4】——檔案系統Linux程式設計
- Linux系統程式設計—有名管道Linux程式設計
- Linux系統程式設計入門Linux程式設計
- Linux系統程式設計基礎Linux程式設計
- 系統程式設計——IPC訊息佇列程式設計佇列
- 【Linux】Linux系統程式設計入門Linux程式設計
- 使用Reactor響應式程式設計React程式設計
- 響應式程式設計RxJava (一)程式設計RxJava
- 響應式程式設計總覽程式設計
- dva 中的響應程式設計程式設計
- 響應式程式設計一覽程式設計