Linux系統(tǒng)編程-信號(hào)入門(mén)
人們很容易高估某個(gè)決定性時(shí)刻的重要性,也很容易低估每天進(jìn)行微小改進(jìn)的價(jià)值。以前我也以為大規(guī)模的成功需要大規(guī)模的行動(dòng),現(xiàn)在我不這么認(rèn)為了。長(zhǎng)期來(lái)看,由于復(fù)利效果,一點(diǎn)小小的改進(jìn)就能產(chǎn)生驚人的變化。
還有一點(diǎn)值得注意的情況,大多數(shù)人有了家庭和子女后,并且現(xiàn)在國(guó)內(nèi)盛行加班文化,很難再集中精力能抽出大塊的時(shí)間進(jìn)行學(xué)習(xí)了,部分還能堅(jiān)持學(xué)習(xí)的人幾乎都是以犧牲睡眠時(shí)間為代價(jià)的,我個(gè)人不太認(rèn)為這種做法,我始終認(rèn)為有更合理健康的方法能形成一個(gè)工作、生活、學(xué)習(xí)、娛樂(lè)的有效循環(huán),或許認(rèn)識(shí)到 微進(jìn)步 的重要性就是一個(gè)很好的開(kāi)始吧。
本文就是我的微進(jìn)步,歡迎閱讀。
一、概述
信號(hào)有時(shí)被稱(chēng)為提供處理異步事件機(jī)制的軟件中斷,與硬件中斷的相似之處在于打斷了程序執(zhí)行的正常流程,很多比較重要的應(yīng)用程序都需處理信號(hào)。事件可以來(lái)自于系統(tǒng)外部,例如用戶(hù)按下 Ctrl+C,或者來(lái)自程序或者內(nèi)核的某些操作。作為一種進(jìn)程間通信 (IPC) 的基本形式,進(jìn)行可以給另一個(gè)進(jìn)程發(fā)送信號(hào)。
信號(hào)很早就是 Unix 的一部分。隨著時(shí)間的推移,信號(hào)有了很大的改進(jìn)。比如在可靠性方面,之前的信號(hào)可能會(huì)出現(xiàn)丟失的情況。在功能方面,現(xiàn)在信號(hào)可以攜帶用戶(hù)定義的附加信息。最初,不同的 Unix 系統(tǒng)對(duì)信號(hào)的修改,后來(lái),POSIX 標(biāo)準(zhǔn)的到來(lái)挽救并且標(biāo)準(zhǔn)化了信號(hào)機(jī)制。
-
用術(shù)語(yǔ) raise 表示一個(gè)信號(hào)的產(chǎn)生,catch 表示接收到一個(gè)信號(hào)。
-
事件的發(fā)生是異步的,程序?qū)π盘?hào)的處理也是異步的。
-
信號(hào)可以被生成、捕獲、響應(yīng)或忽略。有兩種信號(hào)不能被忽略:SIGKILL 和 SIGSTOP。不能被忽略的原因是:它們向內(nèi)核和超級(jí)用戶(hù)提供了使進(jìn)程終止或停止的可靠方法。
1. 簡(jiǎn)單概念
信號(hào)類(lèi)型:
$ man 7 signal
DESCRIPTION
Standard signals
First the signals described in the original POSIX.1-1990 standard.
Signal Value Action Comment
──────────────────────────────────────────────────────────────────────
SIGHUP 1 Term Hangup detected on controlling terminal
or death of controlling process
SIGINT 2 Term Interrupt from keyboard
SIGQUIT 3 Core Quit from keyboard
SIGILL 4 Core Illegal Instruction
SIGABRT 6 Core Abort signal from abort(3)
SIGFPE 8 Core Floating point exception
SIGKILL 9 Term Kill signal
SIGSEGV 11 Core Invalid memory reference
SIGPIPE 13 Term Broken pipe: write to pipe with no
readers
SIGALRM 14 Term Timer signal from alarm(2)
SIGTERM 15 Term Termination signal
SIGUSR1 30,10,16 Term User-defined signal 1
SIGUSR2 31,12,17 Term User-defined signal 2
SIGCHLD 20,17,18 Ign Child stopped or terminated
SIGCONT 19,18,25 Cont Continue if stopped
SIGSTOP 17,19,23 Stop Stop process
SIGTSTP 18,20,24 Stop Stop typed at terminal
SIGTTIN 21,21,26 Stop Terminal input for background process
SIGTTOU 22,22,27 Stop Terminal output for background process
The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
Next the signals not in the POSIX.1-1990 standard but described in SUSv2 and POSIX.1-2001.
Signal Value Action Comment
────────────────────────────────────────────────────────────────────
SIGBUS 10,7,10 Core Bus error (bad memory access)
SIGPOLL Term Pollable event (Sys V).
Synonym for SIGIO
SIGPROF 27,27,29 Term Profiling timer expired
SIGSYS 12,31,12 Core Bad argument to routine (SVr4)
SIGTRAP 5 Core Trace/breakpoint trap
SIGURG 16,23,21 Ign Urgent condition on socket (4.2BSD)
SIGVTALRM 26,26,28 Term Virtual alarm clock (4.2BSD)
SIGXCPU 24,24,30 Core CPU time limit exceeded (4.2BSD)
SIGXFSZ 25,25,31 Core File size limit exceeded (4.2BSD)
...
Next various other signals.
Signal Value Action Comment
────────────────────────────────────────────────────────────────────
SIGIOT 6 Core IOT trap. A synonym for SIGABRT
SIGEMT 7,-,7 Term
SIGSTKFLT -,16,- Term Stack fault on coprocessor (unused)
SIGIO 23,29,22 Term I/O now possible (4.2BSD)
SIGCLD -,-,18 Ign A synonym for SIGCHLD
SIGPWR 29,30,19 Term Power failure (System V)
SIGINFO 29,-,- A synonym for SIGPWR
SIGLOST -,-,- Term File lock lost (unused)
SIGWINCH 28,28,20 Ign Window resize signal (4.3BSD, Sun)
SIGUNUSED -,31,- Core Synonymous with SIGSYS
(Signal 29 is SIGINFO / SIGPWR on an alpha but SIGLOST on a sparc.)
發(fā)送信號(hào):
-
如果想發(fā)送一個(gè)信號(hào)給進(jìn)程,而該進(jìn)程并不是當(dāng)前的前臺(tái)進(jìn)程,就需要使用kill 命令。
-
kill 命令有一個(gè)有用的變體叫 killall,它可以給運(yùn)行著某一命令的所有進(jìn)程發(fā)送信號(hào)。
處理信號(hào):
Unix 系統(tǒng)提供了兩種方法來(lái)改變信號(hào)處置:signal() 和 sigaction()。signal()系統(tǒng)調(diào)用是設(shè)置信號(hào)處置的原始 API,所提供的接口比sigaction() 簡(jiǎn)單。另一方面,sigaction() 提供了 signal() 所不具備的功能。進(jìn)一步而言,signal() 的行為在不同 Unix 實(shí)現(xiàn)間存在差異,這意味著對(duì)可移植性有所追求的程序絕不能使用此調(diào)用來(lái)建立信號(hào)處理函數(shù) (signal handler)。故此,sigaction()是建立信號(hào)處理器的首選API。
由于可能會(huì)在許多老程序中看到 signal() 的應(yīng)用,我們先了解如何用 signal() 函數(shù)來(lái)處理信號(hào)。
signal() 的定義:
$ man 2 signal
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
-
參數(shù)1 signum 指定希望修改 handler 的信號(hào)編號(hào),參數(shù)2 handler,則指定信號(hào)抵達(dá)時(shí)所調(diào)用的 signal handler 函數(shù)的地址。
-
成功,返回以前的信號(hào)處理函數(shù);出錯(cuò),返回 SIG_ERR;
2. 入門(mén)實(shí)驗(yàn)
簡(jiǎn)單試用 signal()。
分解代碼:
static void ouch(int sig) {
printf("OUCH! - I got signal %d\n", sig);
(void) signal(SIGINT, SIG_DFL);
}
int main() {
(void) signal(SIGINT, ouch);
while(1) {
printf("Hello World!\n");
sleep(1);
}
}
運(yùn)行效果:
$ ./ctrlc1
Hello World!
Hello World!
^COUCH! - I got signal 2
Hello World!
Hello World!
相關(guān)要點(diǎn):
-
在信號(hào)處理函數(shù)中,調(diào)用如 printf 這樣的函數(shù)是不安全的。一般的做法是:在信號(hào)處理函數(shù)中設(shè)置一個(gè)標(biāo)志,然后在主程序中檢查該標(biāo)志,如需要就打印一條消息。
-
如果想保留信號(hào)處理函數(shù),讓它繼續(xù)響應(yīng)用戶(hù)的 Ctrl+C 組合鍵,我們就需要再次調(diào)用 signal 函數(shù)來(lái)重新建立它。這會(huì)使信號(hào)在一段時(shí)間內(nèi)無(wú)法得到處理,這段時(shí)間從調(diào)用中斷函數(shù)開(kāi)始,到信號(hào)處理函數(shù)的重建為止。如果在這段時(shí)間內(nèi)程序接收到第二個(gè)信號(hào),它就會(huì)違背我們的意愿終止程序的運(yùn)行。
-
不推薦使用 signal 接口。之所以介紹它,是因?yàn)榭赡軙?huì)在許多老程序中看到它的應(yīng)用。更清晰、執(zhí)行更可靠的函數(shù): sigaction(),在所有的新程序中都應(yīng)該使用這個(gè)函數(shù),暫不做深入介紹。
二、發(fā)送信號(hào)
1. 如何發(fā)送信號(hào)
進(jìn)程可以通過(guò)調(diào)用 kill 函數(shù)向包括它本身在內(nèi)的其他進(jìn)程發(fā)送一個(gè)信號(hào)。
kill():
$ man 2 kill
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
把參數(shù) sig 給指定的信號(hào)發(fā)送給由參數(shù) pid 指定的進(jìn)程號(hào)所指定的進(jìn)程。
kill 調(diào)用會(huì)在失敗時(shí)返回 -1 并設(shè)置 errno 變量,失敗的原因:
-
給定的信號(hào)無(wú)效(errno設(shè)置為EINVAL);
-
發(fā)送進(jìn)程權(quán)限不夠(errno設(shè)置為EPERM);
-
目標(biāo)進(jìn)程不存在(errno設(shè)置為ESRCH);
關(guān)于權(quán)限:
要想發(fā)送一個(gè)信號(hào),發(fā)送進(jìn)程必須擁有相應(yīng)的權(quán)限,包括2種情況:
-
兩個(gè)進(jìn)程必須擁有相同的用戶(hù) ID,即你只能發(fā)送信號(hào)給屬于自己的進(jìn)程;
-
超級(jí)用戶(hù)可以發(fā)送信號(hào)給任何進(jìn)程;
2. 鬧鐘功能
進(jìn)程可以通過(guò)調(diào)用 alarm() 函數(shù)在經(jīng)過(guò)預(yù)定時(shí)間后發(fā)送一個(gè) SIGALRM 信號(hào)。
alarm():
$ man 2 alarm
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
-
在 seconds 秒之后發(fā)送一個(gè) SIGALRM 信號(hào)。
-
返回值是以前設(shè)置的鬧鐘時(shí)間的余留秒數(shù),如果調(diào)用失敗則返回 -1。
相關(guān)要點(diǎn):
-
由于處理的延時(shí)和時(shí)間調(diào)度的不確定性,實(shí)際鬧鐘時(shí)間將比預(yù)先安排的要稍微拖后一點(diǎn)兒。
-
把參數(shù) seconds 設(shè)置為 0 將取消所有已設(shè)置的鬧鐘請(qǐng)求。
-
如果在接收到 SIGALRM 信號(hào)之前再次調(diào)用 alarm() 函數(shù),則鬧鐘重新開(kāi)始計(jì)時(shí)
-
每個(gè)進(jìn)程只能有一個(gè)鬧鐘時(shí)間。
3. 入門(mén)實(shí)驗(yàn)
用 kill() 模擬鬧鐘。
分解代碼:
設(shè)置 signal handler:
int main()
{
pid_t pid;
printf("alarm application starting\n");
pid = fork();
switch(pid) {
case -1:
/* Failure */
perror("fork failed");
exit(1);
case 0:
/* child */
sleep(5);
kill(getppid(), SIGALRM);
exit(0);
}
/* parent */
printf("waiting for alarm to go off\n");
(void) signal(SIGALRM, ding);
pause();
if (alarm_fired)
printf("Ding!\n");
printf("done\n");
exit(0);
}
定義 signal handler:
static int alarm_fired = 0;
static void ding(int sig)
{
alarm_fired = 1;
}
通過(guò) fork 調(diào)用啟動(dòng)新的進(jìn)程:子進(jìn)程休眠 5 秒后向其父進(jìn)程發(fā)送一個(gè) SIGALRM 信號(hào)。父進(jìn)程在安排好捕獲 SIGALRM 信號(hào)后暫停運(yùn)行,直到接收到一個(gè)信號(hào)為止。
運(yùn)行效果:
$ ./alarm
alarm application starting
waiting for alarm to go off
<等待5 秒鐘>
Ding!
done
相關(guān)要點(diǎn):
-
pause() 把程序的執(zhí)行掛起直到有一個(gè)信號(hào)出現(xiàn)為止。使用信號(hào)并掛起程序的執(zhí)行是 Unix 程序設(shè)計(jì)中的一個(gè)重要部分。
$ man 2 pause
#include <unistd.h>
int pause(void); -
當(dāng)它被一個(gè)信號(hào)中斷時(shí),將返回 -1(如果下一個(gè)接收到的信號(hào)沒(méi)有導(dǎo)致程序終止的話(huà))并把 errno 設(shè)置為 EINTR。
-
更常見(jiàn)的方法是使用 sigsuspend() 函數(shù),暫不做介紹。
-
在信號(hào)處理函數(shù)中沒(méi)有調(diào)用 printf,而是通過(guò)設(shè)置標(biāo)志,然后在main函數(shù)中檢查該標(biāo)志來(lái)完成消息的輸出。
-
如果信號(hào)出現(xiàn)在系統(tǒng)調(diào)用的執(zhí)行過(guò)程中會(huì)怎么樣?
-
一般只需要考慮“慢”系統(tǒng)調(diào)用,例如從終端讀數(shù)據(jù),如果在這個(gè)系統(tǒng)調(diào)用等待數(shù)據(jù)時(shí)出現(xiàn)一個(gè)信號(hào),它就會(huì)返回錯(cuò)誤 EINTR。 $ man 3 errno
EINTR
Interrupted function call (POSIX.1); see signal(7). -
如果你開(kāi)始在自己的程序中使用信號(hào),就需要注意一些系統(tǒng)調(diào)用會(huì)因?yàn)榻邮盏搅艘粋€(gè)信號(hào)而失敗。
-
我們需要更健壯的信號(hào)接口:
-
在編寫(xiě)程序中處理信號(hào)部分的代碼時(shí)必須非常小心,因?yàn)樵谑褂眯盘?hào)的程序中會(huì)出現(xiàn)各種各樣的“競(jìng)態(tài)條件”。例如,如果想調(diào)用pause等待一個(gè)信號(hào),可信號(hào)卻出現(xiàn)在調(diào)用 pause() 之前,就會(huì)使程序無(wú)限期地等待一個(gè)不會(huì)發(fā)生的事件。
-
POSIX 標(biāo)準(zhǔn)推薦了一個(gè)更新和更健壯的信號(hào)編程接口:sigaction。
三、信號(hào)集 (Signal Set)
多個(gè)信號(hào)可使用一個(gè)稱(chēng)之為信號(hào)集的數(shù)據(jù)結(jié)構(gòu)來(lái)表示,POSIX.1 定義了數(shù)據(jù)類(lèi)型 sigset_t 以表示一個(gè)信號(hào)集,并且定義了下列 5 個(gè)處理信號(hào)集的函數(shù):
$ man 3 sigemptyset
NAME
sigemptyset, sigfillset, sigaddset, sigdelset, sigismember - POSIX signal set operations
SYNOPSIS
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
-
函數(shù) sigemptyset() 初始化由參數(shù) set 指向的信號(hào)集,清除其中所有信號(hào)。
-
函數(shù) sigfillset() 初始化由參數(shù) set 指向的信號(hào)集,使其包括所有信號(hào)。
-
必須使用 sigemptyset() 或者 sigfillset() 來(lái)初始化信號(hào)集。這是因?yàn)?C 語(yǔ)言不會(huì)對(duì)自動(dòng)變量進(jìn)行初始化,并且,借助于將靜態(tài)變量初始化為 0 的機(jī)制來(lái)表示空信號(hào)集的作法在可移植性上存在問(wèn)題,因?yàn)橛锌赡苁褂梦谎诖a之外的結(jié)構(gòu)來(lái)實(shí)現(xiàn)信號(hào)集。
-
函數(shù) sigaddset() 將一個(gè)信號(hào)添加到已有的信號(hào)集中,sigdelset() 則從信號(hào)集中刪除一個(gè)信號(hào)。
-
sigismember() 函數(shù)用來(lái)測(cè)試信號(hào) sig 是否是信號(hào)集 set 的成員。
四、信號(hào)屏蔽字 (Signal Mask)
4.1 基礎(chǔ)概念
每個(gè)進(jìn)程都有一個(gè)信號(hào)屏蔽字(或稱(chēng)信號(hào)掩碼,signal mask),它規(guī)定了當(dāng)前要阻塞遞送到該進(jìn)程的信號(hào)集。對(duì)于每種信號(hào),屏蔽字中都有一位與之對(duì)應(yīng)。對(duì)于某種信號(hào),若其對(duì)應(yīng)位被設(shè)置,則它當(dāng)前是被阻塞的。進(jìn)程可以調(diào)用 sigprocmask() 檢測(cè)或更改,或同時(shí)進(jìn)行檢測(cè)和更改進(jìn)程的信號(hào)屏蔽字。
向信號(hào)屏蔽字中添加信號(hào)的3種方式:
-
當(dāng)調(diào)用信號(hào)處理器 (signal handler) 時(shí),可能會(huì)引發(fā)信號(hào)自動(dòng)添加到信號(hào)屏蔽字中的行為,暫不作深入介紹。
-
使用 sigaction() 函數(shù)建立信號(hào)處理器時(shí),可以指定一組信號(hào)集,當(dāng)調(diào)用該處理器時(shí)會(huì)將該信號(hào)集里的信號(hào)阻塞,暫不作深入介紹。
-
使用sigprocmask()系統(tǒng)調(diào)用,可以隨時(shí)顯式地向信號(hào)屏蔽字中添加或移除信號(hào)。
先來(lái)了解 sigprocmask():
$ man 2 sigprocmask
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
相關(guān)知識(shí)點(diǎn):
-
sigprocmask() 既可用于修改 進(jìn)程的信號(hào)屏蔽字,也可用于獲取現(xiàn)有的屏蔽字,或者同時(shí)執(zhí)行這2個(gè)操作。
-
參數(shù) how 指定了 sigprocmask() 該如何操作信號(hào)屏蔽字。
-
SIG_BLOCK: 將參數(shù) set 信號(hào)集內(nèi)的信號(hào)添加到信號(hào)屏蔽字中; -
SIG_UNBLOCK: 將參數(shù) set 信號(hào)集內(nèi)的信號(hào)從信號(hào)屏蔽字中移除; -
SIG_SETMASK: 將參數(shù) set 信號(hào)集賦給信號(hào)屏蔽字。 -
若 set 參數(shù)不為空,則其指向一個(gè) sigset_t 緩沖區(qū),用于返回之前的信號(hào)屏蔽字。
-
SUSv3 規(guī)定,如果有任何正在等待的信號(hào) (pending signals) 因調(diào)用了 sigprocmask() 解除了鎖定,那么在此調(diào)用返回前至少會(huì)傳遞一次這些信號(hào)。
-
系統(tǒng)將忽略試圖阻塞 SIGKILL 和 SIGSTOP 信號(hào)的請(qǐng)求。如果試圖阻塞這些信號(hào),sigprocmask() 既不會(huì)予以關(guān)注,也不會(huì)產(chǎn)生錯(cuò)誤。
-
常見(jiàn)的使用方法:
sigset_t blockSet, prevMask;
sigemptyset(&blockSet);
/* 1. Block SIGINT, save previous signal mask */
sigaddset(&blockSet, SIGINT);
if (sigprocmask(SIG_BLOCK, &blockSet, &prevMask) == -1)
errExit("sigprocmask1");
/* 2. Code that should not be interrupted by SIGINT */
/* 3. Restore previous signal mask, unblocking SIGINT */
if (sigprocmask(SIG_SETMASK, &prevMask, NULL) == -1)
errExit("sigprocmask2");
4.2 實(shí)驗(yàn) demo
main() 函數(shù):
1> 為所有信號(hào)注冊(cè)同一個(gè)信號(hào)處理函數(shù),用于驗(yàn)證信號(hào)集是否被成功屏蔽:
static void handler(int sig)
{
if (sig == SIGINT)
gotSigint = 1;
else
sigCnt[sig]++;
}
int main(int argc, char *argv[])
{
int n, numSecs;
sigset_t fullMask, emptyMask;
printf("%s: PID is %ld\n", argv[0], (long) getpid());
for (n = 1; n < NSIG; n++)
(void) signal(n, handler); // UNSAFE
...
}
注意:siganl() 是不可靠的,這里為了簡(jiǎn)化程序而采用該接口。
2> 初始化信號(hào)集,然后屏蔽所有信號(hào):
sigfillset(&fullMask);
if (sigprocmask(SIG_SETMASK, &fullMask, NULL) == -1) {
perror("sigprocmask");
exit(EXIT_FAILURE);
}
printf("%s: sleeping for %d seconds\n", argv[0], numSecs);
sleep(numSecs);
先屏蔽所有的信號(hào),然后睡眠。睡眠期間,進(jìn)程無(wú)法響應(yīng)除 SIGSTOP 和 SIGKILL 之外的任何信號(hào)。
3> 睡眠結(jié)束后,用空信號(hào)集來(lái)解除所有的信號(hào)屏蔽:
sigemptyset(&emptyMask); /* Unblock all signals */
if (sigprocmask(SIG_SETMASK, &emptyMask, NULL) == -1) {
perror("sigprocmask");
exit(EXIT_FAILURE);
}
while (!gotSigint) /* Loop until SIGINT caught */
continue;
for (n = 1; n < NSIG; n++)
if (sigCnt[n] != 0)
printf("%s: signal %d caught %d time%s\n", argv[0], n,
sigCnt[n], (sigCnt[n] == 1) ? "" : "s");
exit(EXIT_SUCCESS);
}
解除了對(duì)某個(gè)等待信號(hào)的屏蔽后,系統(tǒng)會(huì)立刻將該信號(hào)傳遞一次給進(jìn)程。
打印信號(hào)集 printSigset():
void printSigset(FILE *of, const char *prefix, const sigset_t *sigset)
{
int sig, cnt;
cnt = 0;
for (sig = 1; sig < NSIG; sig++) {
if (sigismember(sigset, sig)) {
cnt++;
fprintf(of, "%s%d (%s)\n", prefix, sig, strsignal(sig));
}
}
if (cnt == 0)
fprintf(of, "%s<empty signal set>\n", prefix);
}
3. 運(yùn)行效果:
屏蔽期間多次按下 ctrl + c (發(fā)送 SIGINT):
$ ./signal_set 5
./signal_set: PID is 18375
blocked:1 (Hangup)
blocked:2 (Interrupt)
blocked:3 (Quit)
...
blocked:64 (Real-time signal 30)
./signal_set: sleeping for 5 seconds
^C^C^Cblocked:<empty signal set>
./signal_set: signal 2 caught 1 time
在信號(hào)被屏蔽的 5 秒期間,連續(xù)按下 3 次 ctrl + c,所有信號(hào)都不會(huì)被處理。當(dāng)過(guò)了 5 秒后,解除信號(hào)屏蔽,僅僅有一次 SIGINT 信號(hào)被成功地傳遞并處理。
五、等待中的信號(hào) (Pending Signals)
如果某進(jìn)程接受了一個(gè)該進(jìn)程正在阻塞的信號(hào),那么會(huì)將該信號(hào)填加到進(jìn)程的等待信號(hào)集中。當(dāng)解除對(duì)該信號(hào)的鎖定時(shí),會(huì)隨之將信號(hào)傳遞給此進(jìn)程。為了確定進(jìn)程中處于等待狀態(tài)的是哪些信號(hào),可以使用 sigpending()。
$ man 2 sigpending
NAME
sigpending, rt_sigpending - examine pending signals
SYNOPSIS
#include <signal.h>
int sigpending(sigset_t *set);
DESCRIPTION
sigpending() returns the set of signals that are pending for delivery to the calling thread (i.e., the signals
which have been raised while blocked). The mask of pending signals is returned in set.
sigpending() 為調(diào)用進(jìn)程返回處于等待狀態(tài)的信號(hào)集,并將其置于 set 指向的sigset_t 中。
相關(guān)知識(shí)點(diǎn):
-
如果修改了對(duì)等待信號(hào)的處置 (術(shù)語(yǔ)disposition),那么當(dāng)后來(lái)解除對(duì)信號(hào)的鎖定時(shí),將根據(jù)新的處置來(lái)處理信號(hào)。
六、待處理的信號(hào) (Pending Signals)
如果某進(jìn)程接受了一個(gè)該進(jìn)程正在阻塞 (blocking) 的信號(hào),那么會(huì)將該信號(hào)填加到進(jìn)程的 等待信號(hào)集 (set of pending signals) 中。當(dāng)解除對(duì)該信號(hào)的阻塞時(shí),會(huì)隨之將信號(hào)傳遞給此進(jìn)程??梢允褂?sigpending() 確定進(jìn)程中處于等待狀態(tài)的是哪些信號(hào)。
$ man 2 sigpending
#include <signal.h>
int sigpending(sigset_t *set);
sigpending() 為調(diào)用進(jìn)程返回處于等待狀態(tài)的信號(hào)集,并將其置于參數(shù) set 指向的 sigset_t 中。
1. 一個(gè)簡(jiǎn)單的例子 (sig_pending.c)
1) 分解代碼:
1> main():
int main(void)
{
sigset_t newmask, oldmask, pendmask;
if (signal(SIGQUIT, sig_quit) == SIG_ERR)
err_sys("can't catch SIGQUIT");
/* Block SIGQUIT and save current signal mask. */
sigemptyset(&newmask);
sigaddset(&newmask, SIGQUIT);
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
err_sys("SIG_BLOCK error");
/* SIGQUIT here will remain pending */
sleep(5);
if (sigpending(&pendmask) < 0)
err_sys("sigpending error");
if (sigismember(&pendmask, SIGQUIT))
printf("\nSIGQUIT pending\n");
/* Restore signal mask which unblocks SIGQUIT. */
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
err_sys("SIG_SETMASK error");
printf("SIGQUIT unblocked\n");
/* SIGQUIT here will terminate with core file */
sleep(5);
exit(0);
}
main() 做了 5 件事:
-
設(shè)置 SIGQUIT 的信號(hào)處理函數(shù); -
屏蔽 SIGQUIT; -
睡眠 5 秒,用于等待 SIGQUIT 信號(hào); -
睡眠結(jié)束,檢測(cè) SIGQUIT 是否處于 pending; -
解除屏蔽 SIGQUIT;
注意:在設(shè)置 SIGQUIT 為阻塞時(shí),我們保存了老的屏蔽字。為了解除對(duì)該信號(hào)的阻塞,用老的屏蔽字重新設(shè)置了進(jìn)程信號(hào)屏蔽字。另一種方法是用 SIG_UNBLOCK 使阻塞的信號(hào)不再阻塞。如果編寫(xiě)一個(gè)可能由其他人使用的函數(shù),而且需要在函數(shù)中阻塞一個(gè)信號(hào),則不能用 SIG_UNBLOCK 簡(jiǎn)單地解除對(duì)此信號(hào)的阻塞,這是因?yàn)榇撕瘮?shù)的調(diào)用者在調(diào)用本函數(shù)之前可能也阻塞了此信號(hào)。
2> 信號(hào)處理函數(shù) sig_quit():
static void sig_quit(int signo)
{
printf("caught SIGQUIT\n");
if (signal(SIGQUIT, SIG_DFL) == SIG_ERR)
err_sys("can't reset SIGQUIT");
}
2) 運(yùn)行效果:
$ ./sig_pending
^\ // 按下 1 次 ctrl + \ (在5s之內(nèi))
SIGQUIT pending // 從 sleep(5) 返回后
caught SIGQUIT // 在信號(hào)處理程序中
SIGQUIT unblocked // 從sigprocmask() 返回
^\Quit (core dumped)
2 個(gè)值得注意的點(diǎn):
-
信號(hào)處理函數(shù)是在 sigprocmask() unblock 信號(hào)返回之前被調(diào)用;
-
用 signal() 設(shè)置信號(hào)處理函數(shù),信號(hào)被處理時(shí),會(huì)將信號(hào)處置重置為其默認(rèn)行為。要想在同一信號(hào)“再度光臨”時(shí)再次調(diào)用該信號(hào)處理器函數(shù),程序員必須在信號(hào)處理器內(nèi)部調(diào)用signal(),以顯式重建處理器函數(shù),但是這種處理方式是不安全的,真實(shí)的項(xiàng)目里應(yīng)使用 sigaction(),后續(xù)的文章會(huì)舉例講解。
七、不對(duì)待處理的信號(hào)進(jìn)行排隊(duì)處理
等待信號(hào)集只是一個(gè)掩碼,僅表明一個(gè)信號(hào)是否發(fā)生,而未表明其發(fā)生的次數(shù)。換言之,如果同一信號(hào)在阻塞狀態(tài)下產(chǎn)生多次,那么會(huì)將該信號(hào)記錄在等待信號(hào)集中,并在稍后僅傳遞一次。后面會(huì)介紹實(shí)時(shí)信號(hào),對(duì)實(shí)時(shí)信號(hào)所采取的是隊(duì)列化管理。如果將某一實(shí)時(shí)信號(hào)的多個(gè)實(shí)例發(fā)送給一進(jìn)程,那么將會(huì)多次傳遞該實(shí)時(shí)信號(hào),暫不做深入介紹。
1. 仍是那個(gè)簡(jiǎn)單的例子 (sig_pending.c)
為了降低學(xué)習(xí)難度,跟前面的 Pending Signals 章節(jié)使用同一個(gè)例子,修改一下測(cè)試步驟:
$ ./sig_pending
^\^\^\ // 按下 3 次 ctrl + \ (在5s之內(nèi))
SIGQUIT pending // 從 sleep(5) 返回后
caught SIGQUIT // 只調(diào)用了一次信號(hào)處理程序
SIGQUIT unblocked // 從sigprocmask() 返回
^\Quit (core dumped)
第二次運(yùn)行該程序時(shí),在進(jìn)程休眠期間產(chǎn)生了 3 次 SIGQUIT 信號(hào),但是取消對(duì)該信號(hào)的阻塞后,系統(tǒng)只向進(jìn)程傳送了一次 SIGQUIT,從中可以看出在 Linux 系統(tǒng)上沒(méi)有對(duì)信號(hào)進(jìn)行排隊(duì)處理。
2. 查看 Linux 內(nèi)核里 Signal Pending 相關(guān)的實(shí)現(xiàn) (非重點(diǎn))
1) 相關(guān)數(shù)據(jù)結(jié)構(gòu)
內(nèi)核用 struct task_struct 來(lái)描述一個(gè)進(jìn)程,struct task_struct 中信號(hào)相關(guān)的成員 (Linux-4.14):
<sched.h>
struct task_struct {
...
/* Signal handlers: */
struct signal_struct *signal;
struct sighand_struct *sighand;
sigset_t blocked;
sigset_t real_blocked;
/* Restored if set_restore_sigmask() was used: */
sigset_t saved_sigmask;
struct sigpending pending;
unsigned long sas_ss_sp;
size_t sas_ss_size;
unsigned int sas_ss_flags;
...
};
我們將注意力集中中 struct sigpending pending 上。struct sigpending pending 建立了一個(gè)鏈表,該鏈表包含了所有已經(jīng)產(chǎn)生、且有待內(nèi)核處理的信號(hào),其定義如下:
struct sigpending {
struct list_head list;
sigset_t signal;
};
-
成員 struct list_head list 通過(guò)雙向鏈表管理所有待處理信號(hào),每一種待處理的信號(hào)對(duì)應(yīng)雙向鏈表中的 1 個(gè) struct sigqueue 節(jié)點(diǎn)。
-
成員 sigset_t signal 是位圖 (bit mask,或稱(chēng)位掩碼),它指定了仍然有待處理的所有信號(hào)的編號(hào)。某 1 bit = 1 表示該 bit 對(duì)應(yīng)的信號(hào)待處理。sigset_t 所包含的比特位數(shù)目要 >= 所支持的信號(hào)數(shù)目。因此,內(nèi)核使用了 unsigned long 數(shù)組來(lái)定義該數(shù)據(jù)類(lèi)型:
typedef struct {
unsigned long sig[_NSIG_WORDS];
} sigset_t;
-
struct sigqueue 的定義如下:
struct sigqueue {
struct list_head list;
int flags;
siginfo_t info;
...
};
-
siginfo_t 用于保存信號(hào)的額外信息,暫時(shí)不用關(guān)心。
注意:在 struct sigpending 鏈表中,struct sigqueue 對(duì)應(yīng)的是一種類(lèi)型的待處理信號(hào),而不是某一個(gè)具體的信號(hào)。
示意圖:
2) 信號(hào)的產(chǎn)生
當(dāng)給進(jìn)程發(fā)送一個(gè)信號(hào)時(shí),這個(gè)信號(hào)可能來(lái)自?xún)?nèi)核,也可能來(lái)自另外一個(gè)進(jìn)程。
內(nèi)核里有多個(gè) API 能產(chǎn)生信號(hào),這些 API 最終都會(huì)調(diào)用 send_signal()。我們重點(diǎn)關(guān)注信號(hào)是何時(shí)被設(shè)置為 pending 狀態(tài)的。
linux/kernel/signal.c:
send_signal()
__send_signal()
struct sigqueue *q = __sigqueue_alloc();
list_add_tail(&q->list, &pending->list); // 將待處理信號(hào)添加到 pending 鏈表中
sigaddset(&pending->signal, sig); // 在位圖中將信號(hào)對(duì)應(yīng)的 bit 置 1
complete_signal(sig, t, group);
signal_wake_up();
send_signal() 會(huì)分配一個(gè)新的 struct sigqueue 實(shí)例,然后為其填充信號(hào)的額外信息,并添加到目標(biāo)進(jìn)程的 sigpending 鏈表且設(shè)置位圖。
如果信號(hào)成功發(fā)送,沒(méi)有被阻塞,就可以用 signal_wake_up() 喚醒目標(biāo)進(jìn)程,使得調(diào)度器可以選擇目標(biāo)進(jìn)程運(yùn)行。
3) 信號(hào)的傳遞:
這些知識(shí)放在這篇文章里已經(jīng)完全超綱了,如果將所有的細(xì)節(jié)都暴露出來(lái)會(huì)讓初學(xué)者感到極度的困惑。
所以,我們只邁出一小步,將僅剩的一點(diǎn)注意力集中在內(nèi)核在執(zhí)行信號(hào)處理函數(shù)前是如何處理 pending 信號(hào)的。
在每次由內(nèi)核態(tài)切換到用戶(hù)態(tài)時(shí),內(nèi)核都會(huì)進(jìn)行信號(hào)處理,最終的效果就是調(diào)用 do_signal() 函數(shù)。
linux/kernel/signal.c:
do_signal()
get_signal()
dequeue_signal(current, ¤t->blocked, &ksig->info);
handle_signal()
signal_setup_done();
signal_delivered();
-
dequeue_signal() 是關(guān)鍵點(diǎn):
dequeue_signal()
int sig = next_signal(pending, mask);
collect_signal(sig, pending, info, resched_timer);
sigdelset(&list->signal, sig); // 取消信號(hào)的 pending 狀態(tài)
list_del_init(&first->list); // 刪除 pending 鏈表中的 struct sigqueue 節(jié)點(diǎn)
copy_siginfo(info, &first->info);
-
handle_signal() 會(huì)操作進(jìn)程在用戶(hù)態(tài)下的棧,使得在從內(nèi)核態(tài)切換到用戶(hù)態(tài)之后運(yùn)行信號(hào)處理程序,而不是正常的程序代碼。
-
do_signal() 返回時(shí),信號(hào)處理函數(shù)就會(huì)被執(zhí)行。
七、相關(guān)參考
-
《Unix 環(huán)境高級(jí)編程-第10章 信號(hào)》 -
《Linux/Unix 系統(tǒng)編程手冊(cè)-第20章 信號(hào):基本概念》 -
《Linux 系統(tǒng)編程-第10章 信號(hào)》 -
《Linux 程序設(shè)計(jì)-第11章 進(jìn)程和信號(hào)》 -
《深入理解 Linux 內(nèi)核 第11章 信號(hào)》 -
《深入 Linux 內(nèi)核架構(gòu) 5.4.1信號(hào)》 -
《Linux 內(nèi)核源代碼情景分析 6.4信號(hào)》
你和我各有一個(gè)蘋(píng)果,如果我們交換蘋(píng)果的話(huà),我們還是只有一個(gè)蘋(píng)果。但當(dāng)你和我各有一個(gè)想法,我們交換想法的話(huà),我們就都有兩個(gè)想法了。如果你也對(duì) 嵌入式系統(tǒng)和開(kāi)源軟件 感興趣,并且想和更多人互相交流學(xué)習(xí)的話(huà),請(qǐng)關(guān)注我的公眾號(hào):嵌入式系統(tǒng)磚家,一起來(lái)學(xué)習(xí)吧,無(wú)論是 關(guān)注或轉(zhuǎn)發(fā) ,還是賞賜,都是對(duì)作者莫大的支持,謝謝 各位的大拇指 ,祝工作順利,家庭和睦~
-END-
推薦閱讀
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問(wèn)題,請(qǐng)聯(lián)系我們,謝謝!