APUE 3 -- 信號 (signal)<II>: 可靠信號
一個事件可以使一個信號發送給一個進程,這個事件可以是硬件異常,可以是軟件條件觸發,可以是終端產生信號,也可以是一個kill函數調用。當信號產生後,內核通常會在進程表中設置某種形式的標誌(flag)。我們可以認為當進程中的信號處理函數被觸發的時候認為信號下達到了(delivered)這個進程。從信號產生到信號下達到進程這段期間,信號被認為是掛起狀態(pending)。進程擁有阻塞信號下達的選項。如果一個阻塞信號要發送給一個進程,而且信號的處理方式是默認處理或者被進程捕獲,那麽這個信號將一直處於掛起狀態(pending)直到這個進程將信號設置成非阻塞狀態或將信號的處理方式改為忽略。操作系統在信號下達(delivered)的時候決定如何處理這個阻塞信號,而不是在信號產生的時候。這樣做允許進程在信號下達前更改信號的處理方式。
如果一個阻塞信號在進程解除它的阻塞前產生多次,那麽unix內核也僅僅會向進程下發一次這個信號。POSIX並沒有規定信號下發到進程的順序,然而與進程當前狀態相關的信號會較先到達進程。
每個進程都有一個信號掩碼(signal mask),它定義了一個當前被阻塞發送到該進程的信號的集合。我們可以認為這個掩碼對於每個可能下發到該進程的信號有一個與之對應的bit位,對於一個給定的信號來說,如果與之對應的bit位是打開狀態,那麽意味著這個信號當前應處於阻塞狀態。進程可以通過sigprocmask來檢查並更改他當前的信號掩碼。因為信號的數量是有可能超過一個整數正bit位位數的,所有POSIX規範定義了一個叫做sigset_t的數據類型,它包含了所有信號集。信號掩碼就是存儲在這其中一個信號集中的。
發送信號
kill & raise
可以使用kill函數向一個進程或進程組發送信號。raise函數允許進程給他自己發送信號。
1 #include <signal.h> 2 3 /* return 0 if OK, -1 on error */ 4 int kill(pid_t pid, int signo); 5 6 /* return 0 if OK, -1 on error */ 7 int raise(int signo);
函數調用 raise(signo); 等同於 kill(getpid(), signo); 。
對於kill函數,pid有以下四種不同的選擇:
- pid > 0: 信號被發送給進程號為pid的進程
- pid == 0: 信號被發送給所有進程組Id等於發送者進程組的進程(即發送給發送進程所屬進程組下的所有進程),前提是發送進程有權限將信號發送給該進程並且該進程不是系統進程。
- pid < 0: 信號被發送給進程組Id為pid絕對值的進程組下的所有進程,前提是發送進程有權限發送信號到該進程並且該進程不是系統進程。
- pid == -1: 信號被發送給發送進程有權限發送的所有進程,但不包含系統進程。
正如之前我們提到的,進程需要足夠的權限才能向另一個進程發送信號,超級用戶可以向任何進程發送信號。對於其他用戶,可以發送信號的基本準則是:發送進程的真實用戶Id(real user Id)或有效用戶Id(effective user Id)必須等於接收進程的真實用戶Id或有效用戶Id。如果實現支持 _POSIX_SAVED_IDS的話,系統會檢查接收進程的saved set-user-ID而不是有效用戶Id。關於發送信號權限的一個特殊情景是:如果待發送信號是SIGCONT,那麽發送進程可以將信號發送給他所屬會話下的任何進程。
POSIX規範定義信號值為0的信號為空信號(null signal)。它可以用於通過kill函數來檢測某一進程是否存在。kill函數在收到值為0的信號後會進程正常的錯誤檢查,但是不會發送此信號。因此我們可以通過 kill(pid, 0) 來判斷進程id為pid的進程是否存在。然而, UNIX系統在一定時間後會循環使用進程 IDs,所以通過pid檢查出來的進程未必真的是你認為的那個進程(即函數調用時,通過pid查詢到的進程未必會是你認為的那個進程)。另外kill函數不是原子的,當kill函數返回時,有可能被發送信號的進程已經結束。
alarm & pause
alarm函數允許我們設置一個定時器,這個定時器在未來某個時間點觸發,這個定時器觸發後會產生一個SIGALRM信號,此信號的默認處理方式是結束進程。
1 #include <unistd.h> 2 3 /* 如果之前沒有設置alarm返回0,否則返回之前 4 設置的alarm所剩余的秒數 */ 5 unsigned int alarm(unsigned int seconds);
一旦到了alarm所設置的時間點,內核就會發送alarm信號,而由於處理器調度延時進程此時可能還無法獲取到此信號處理的控制權。每個進程中只有一個alarm時鐘。當我們調用alarm時,如果當前進程之前註冊的鬧鐘還未到期,那麽此函數返回之前鬧鐘距到期剩余的秒數,並且之前註冊的鬧鐘會被這個新的鬧鐘值取代。另外,如果之前註冊的鬧鐘還未到期並且新註冊的鬧鐘值為0的話,那麽之前註冊的鬧鐘會被取消。
pause函數的調用會阻塞調用進程直到調用進程捕獲到一個信號。
1 #include <unistd.h> 2 3 /*Returns: -1 with errno set to EINTR*/ 4 int pause(void);
信號集
像我們之前提到的,不同信號的數量可能會超過一個整數的bit位所能表示的信號數量。POSIX規範定義了sigset_t類型用來表示信號集,並使用下面的5個函數來管理信號集:
1 #include <unistd.h> 2 3 /*All four return 0 if OK, -1 on error*/ 4 5 /*清空set信號集中的所有信號*/ 6 int sigemptyset(sigset_t* set); 7 /*使set包含所有信號*/ 8 int sigfillset(sigset_t* set); 9 /*將信號signo加入到信號集set中*/ 10 int sigaddset(sigset_t* set, int signo); 11 /*從信號集set中刪除signo*/ 12 int sigdelset(sigset_t* set, int signo); 13 14 /*Returns 1 if true, 0 if false, -1 on error*/ 15 int sigismember(const sigset_t* set, int signo);
sigprocmask
一個進程的信號掩碼是指被阻止下發到此進程的所有信號的集合。進程可以檢查並更改他的信號掩碼。
#include <signal.h> /*
如果oset不為空,那麽此進程信號掩碼之前的值會被復制到oset中。
如果set為空,那麽此進程的信號掩碼不會被更改,而信號掩碼的當前值
也不會復制到oset中。
*/ int sigprocmask(int how, const sigset_t* restrict set, sigset_t* restrict oset);
how參數的值指明如何修改信號掩碼:
- SIG_BLOCK: set包含另外的我們想阻塞的信號
- SIG_UNBLOCK:set包含我們想要解除阻塞的信號
- SIG_SETMASK:使用set替代進程當前信號掩碼
sigprocmask 不支持多線程環境。
sigpending
#include <unistd.h> /*通過set返回發送給當前進程但被阻塞的信號*/ int sigpending(sigset_t* set);
sigaction
我們可以通過sigaction方法檢查並修改特定信號的處理方式(action)。他是早期sinal函數的取代版本。
#include <unistd.h> /*若oact不為空,函數通過oact返回當前signo的action * 若act不為空,則修改signo的當前action*/ int sigaction(int signo, const struct sigaction* restrict act, struct sigaction* restrict oact); struct sigaction{ void (*sa_handler)(int); /*addr of signal handler,
or SIG_IGN or SIG_DFL */ sigset_t sa_mask; /*additional signals to block*/ int sa_flag; /*signal options*/ /*alternate handler*/ void (*sa_sigaction)(int,siginfo_t *, void *); };
當使用sigaction改變signo的action時,如果sa_handler指向了一個信號處理函數(SIG_IGN和SIG_DFL除外),sigaction函數會將sa_mask指向的信號集合在這個信號處理函數(sa_handler)被調用前加入到當前進程掩碼中,當信號處理函數返回時,進程的信號掩碼會恢復為他原來的值。這樣,使我們能夠在信號處理函數被調用是阻塞一部分信號的到達。一旦我們為一個信號安裝了action, 那麽對於這個信號這個action將一直處於安裝狀態,除非我們使用sigaction方法明確的更改可它。
sa_flags:
sigsuspend
等待信號到達的一個整潔而可靠地方式是先阻塞這個信號然後使用sigsuspend。
#include <signal.h> /* 將當前進程信號掩碼設置為sigmask, 函數返回後將進程掩碼恢復為調用前 的值, 該函數總是返回-1,並設置 errno 為 -1 */ int sigsuspend(const sigset_t* sigmask);
sigsuspend 可用於等待指定信號的到達,他的常用用法如下:
1 sigset_t mask, oldmask; 2 3 … 4 5 /* Set up the mask of signals to temporarily block. */ 6 sigemptyset (&mask); 7 sigaddset (&mask, SIGUSR1); 8 9 … 10 11 /* Wait for a signal to arrive. */ 12 sigprocmask (SIG_BLOCK, &mask, &oldmask); 13 while (!usr_interrupt) 14 sigsuspend (&oldmask); 15 sigprocmask (SIG_UNBLOCK, &mask, NULL);
通過user_interrupt 判斷是否等待的SIGUSR1信號已到達,sigsuspend再返回時將進程信號掩碼設置為他被調用前的值,因此我們最後需要將添加mask移除掉。
Signal Names and Numbers
數組sys_siglist可以幫助我們匹配信號與信號名:
1 extern char* sys_siglist[]; 數組索引為信號值, 數組元素值為信號名。
信號與信號名的轉換:
1 #include <signal.h> 2 3 /* 如果msg不為空,則向stderr 輸出msg緊跟一個冒號加一個空著在加信號描述;如果msg為空則只向stderr輸出信號描述*/ 4 void psignal(int signo, const char* msg); 5 6 void psiginfo(const siginfo_t info, const char* msg); 7 8 /*獲取信號描述*/ 9 char* strsignal(int signo); 10 11 void sig2str(int signo, char* str); 12 void str2sig(const char* str, int* signop);
總結
信號通常用於一些相對復雜的程序, 理解如何及為何處理信號對於UNIX高級編程是必要的。
APUE 3 -- 信號 (signal)<II>: 可靠信號