3. 阻塞信號

3.1. 信號在內核中的表示

以上我們討論了信號產生(Generation)的各種原因,而實際執行信號的處理動作稱為信號遞達(Delivery),信號從產生到遞達之間的狀態,稱為信號未決(Pending)。進程可以選擇阻塞(Block)某個信號。被阻塞的信號產生時將保持在未決狀態,直到進程解除對此信號的阻塞,才執行遞達的動作。注意,阻塞和忽略是不同的,只要信號被阻塞就不會遞達,而忽略是在遞達之後可選的一種處理動作。信號在內核中的表示可以看作是這樣的:

圖 33.1. 信號在內核中的表示示意圖

信號在內核中的表示示意圖

每個信號都有兩個標誌位分別表示阻塞和未決,還有一個函數指針表示處理動作。信號產生時,內核在進程控制塊中設置該信號的未決標誌,直到信號遞達才清除該標誌。在上圖的例子中,

  1. SIGHUP信號未阻塞也未產生過,當它遞達時執行預設處理動作。

  2. SIGINT信號產生過,但正在被阻塞,所以暫時不能遞達。雖然它的處理動作是忽略,但在沒有解除阻塞之前不能忽略這個信號,因為進程仍有機會改變處理動作之後再解除阻塞。

  3. SIGQUIT信號未產生過,一旦產生SIGQUIT信號將被阻塞,它的處理動作是用戶自定義函數sighandler

如果在進程解除對某信號的阻塞之前這種信號產生過多次,將如何處理?POSIX.1允許系統遞送該信號一次或多次。Linux是這樣實現的:常規信號在遞達之前產生多次只計一次,而實時信號在遞達之前產生多次可以依次放在一個隊列裡。本章不討論實時信號。從上圖來看,每個信號只有一個bit的未決標誌,非0即1,不記錄該信號產生了多少次,阻塞標誌也是這樣表示的。因此,未決和阻塞標誌可以用相同的數據類型sigset_t來存儲,sigset_t稱為信號集,這個類型可以表示每個信號的“有效”或“無效”狀態,在阻塞信號集中“有效”和“無效”的含義是該信號是否被阻塞,而在未決信號集中“有效”和“無效”的含義是該信號是否處于未決狀態。下一節將詳細介紹信號集的各種操作。阻塞信號集也叫做當前進程的信號屏蔽字(Signal Mask),這裡的“屏蔽”應該理解為阻塞而不是忽略。

3.2. 信號集操作函數

sigset_t類型對於每種信號用一個bit表示“有效”或“無效”狀態,至于這個類型內部如何存儲這些bit則依賴于系統實現,從使用者的角度是不必關心的,使用者只能調用以下函數來操作sigset_t變數,而不應該對它的內部數據做任何解釋,比如用printf直接打印sigset_t變數是沒有意義的。

#include <signal.h>

int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);

函數sigemptyset初始化set所指向的信號集,使其中所有信號的對應bit清零,表示該信號集不包含任何有效信號。函數sigfillset初始化set所指向的信號集,使其中所有信號的對應bit置位,表示該信號集的有效信號包括系統支持的所有信號。注意,在使用sigset_t類型的變數之前,一定要調用sigemptysetsigfillset做初始化,使信號集處于確定的狀態。初始化sigset_t變數之後就可以在調用sigaddsetsigdelset在該信號集中添加或刪除某種有效信號。這四個函數都是成功返回0,出錯返回-1。sigismember是一個布爾函數,用於判斷一個信號集的有效信號中是否包含某種信號,若包含則返回1,不包含則返回0,出錯返回-1。

3.3. sigprocmask

調用函數sigprocmask可以讀取或更改進程的信號屏蔽字。

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

返回值:若成功則為0,若出錯則為-1

如果oset是非空指針,則讀取進程的當前信號屏蔽字通過oset參數傳出。如果set是非空指針,則更改進程的信號屏蔽字,參數how指示如何更改。如果osetset都是非空指針,則先將原來的信號屏蔽字備份到oset裡,然後根據sethow參數更改信號屏蔽字。假設當前的信號屏蔽字為mask,下表說明了how參數的可選值。

表 33.1. how參數的含義

SIG_BLOCKset包含了我們希望添加到當前信號屏蔽字的信號,相當於mask=mask|set
SIG_UNBLOCKset包含了我們希望從當前信號屏蔽字中解除阻塞的信號,相當於mask=mask&~set
SIG_SETMASK設置當前信號屏蔽字為set所指向的值,相當於mask=set

如果調用sigprocmask解除了對當前若干個未決信號的阻塞,則在sigprocmask返回前,至少將其中一個信號遞達。

3.4. sigpending

#include <signal.h>

int sigpending(sigset_t *set);

sigpending讀取當前進程的未決信號集,通過set參數傳出。調用成功則返回0,出錯則返回-1。

下面用剛學的幾個函數做個實驗。程序如下:

#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void printsigset(const sigset_t *set)
{
	int i;
	for (i = 1; i < 32; i++)
		if (sigismember(set, i) == 1)
			putchar('1');
		else
			putchar('0');
	puts("");
}

int main(void)
{
	sigset_t s, p;
	sigemptyset(&s);
	sigaddset(&s, SIGINT);
	sigprocmask(SIG_BLOCK, &s, NULL);
	while (1) {
		sigpending(&p);
		printsigset(&p);
		sleep(1);
	}
	return 0;
}

程序運行時,每秒鐘把各信號的未決狀態打印一遍,由於我們阻塞了SIGINT信號,按Ctrl-C將會使SIGINT信號處于未決狀態,按Ctrl-\仍然可以終止程序,因為SIGQUIT信號沒有阻塞。

$ ./a.out 
0000000000000000000000000000000
0000000000000000000000000000000(這時按Ctrl-C)
0100000000000000000000000000000
0100000000000000000000000000000(這時按Ctrl-\)
Quit (core dumped)