Linux系統(tǒng)編程-信號入門
人們很容易高估某個決定性時刻的重要性,也很容易低估每天進(jìn)行微小改進(jìn)的價值。以前我也以為大規(guī)模的成功需要大規(guī)模的行動,現(xiàn)在我不這么認(rèn)為了。長期來看,由于復(fù)利效果,一點(diǎn)小小的改進(jìn)就能產(chǎn)生驚人的變化。
還有一點(diǎn)值得注意的情況,大多數(shù)人有了家庭和子女后,并且現(xiàn)在國內(nèi)盛行加班文化,很難再集中精力能抽出大塊的時間進(jìn)行學(xué)習(xí)了,部分還能堅(jiān)持學(xué)習(xí)的人幾乎都是以犧牲睡眠時間為代價的,我個人不太認(rèn)為這種做法,我始終認(rèn)為有更合理健康的方法能形成一個工作、生活、學(xué)習(xí)、娛樂的有效循環(huán),或許認(rèn)識到 微進(jìn)步 的重要性就是一個很好的開始吧。
本文就是我的微進(jìn)步,歡迎閱讀。
一、概述
信號有時被稱為提供處理異步事件機(jī)制的軟件中斷,與硬件中斷的相似之處在于打斷了程序執(zhí)行的正常流程,很多比較重要的應(yīng)用程序都需處理信號。事件可以來自于系統(tǒng)外部,例如用戶按下 Ctrl+C,或者來自程序或者內(nèi)核的某些操作。作為一種進(jìn)程間通信 (IPC) 的基本形式,進(jìn)行可以給另一個進(jìn)程發(fā)送信號。
信號很早就是 Unix 的一部分。隨著時間的推移,信號有了很大的改進(jìn)。比如在可靠性方面,之前的信號可能會出現(xiàn)丟失的情況。在功能方面,現(xiàn)在信號可以攜帶用戶定義的附加信息。最初,不同的 Unix 系統(tǒng)對信號的修改,后來,POSIX 標(biāo)準(zhǔn)的到來挽救并且標(biāo)準(zhǔn)化了信號機(jī)制。
-
用術(shù)語 raise 表示一個信號的產(chǎn)生,catch 表示接收到一個信號。
-
事件的發(fā)生是異步的,程序?qū)π盘柕奶幚硪彩钱惒降摹?/p>
-
信號可以被生成、捕獲、響應(yīng)或忽略。有兩種信號不能被忽略:SIGKILL 和 SIGSTOP。不能被忽略的原因是:它們向內(nèi)核和超級用戶提供了使進(jìn)程終止或停止的可靠方法。
1. 簡單概念
信號類型:
$ 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ā)送信號:
-
如果想發(fā)送一個信號給進(jìn)程,而該進(jìn)程并不是當(dāng)前的前臺進(jìn)程,就需要使用kill 命令。
-
kill 命令有一個有用的變體叫 killall,它可以給運(yùn)行著某一命令的所有進(jìn)程發(fā)送信號。
處理信號:
Unix 系統(tǒng)提供了兩種方法來改變信號處置:signal() 和 sigaction()。signal()系統(tǒng)調(diào)用是設(shè)置信號處置的原始 API,所提供的接口比sigaction() 簡單。另一方面,sigaction() 提供了 signal() 所不具備的功能。進(jìn)一步而言,signal() 的行為在不同 Unix 實(shí)現(xiàn)間存在差異,這意味著對可移植性有所追求的程序絕不能使用此調(diào)用來建立信號處理函數(shù) (signal handler)。故此,sigaction()是建立信號處理器的首選API。
由于可能會在許多老程序中看到 signal() 的應(yīng)用,我們先了解如何用 signal() 函數(shù)來處理信號。
signal() 的定義:
$ man 2 signal
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
-
參數(shù)1 signum 指定希望修改 handler 的信號編號,參數(shù)2 handler,則指定信號抵達(dá)時所調(diào)用的 signal handler 函數(shù)的地址。
-
成功,返回以前的信號處理函數(shù);出錯,返回 SIG_ERR;
2. 入門實(shí)驗(yà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):
-
在信號處理函數(shù)中,調(diào)用如 printf 這樣的函數(shù)是不安全的。一般的做法是:在信號處理函數(shù)中設(shè)置一個標(biāo)志,然后在主程序中檢查該標(biāo)志,如需要就打印一條消息。
-
如果想保留信號處理函數(shù),讓它繼續(xù)響應(yīng)用戶的 Ctrl+C 組合鍵,我們就需要再次調(diào)用 signal 函數(shù)來重新建立它。這會使信號在一段時間內(nèi)無法得到處理,這段時間從調(diào)用中斷函數(shù)開始,到信號處理函數(shù)的重建為止。如果在這段時間內(nèi)程序接收到第二個信號,它就會違背我們的意愿終止程序的運(yùn)行。
-
不推薦使用 signal 接口。之所以介紹它,是因?yàn)榭赡軙谠S多老程序中看到它的應(yīng)用。更清晰、執(zhí)行更可靠的函數(shù): sigaction(),在所有的新程序中都應(yīng)該使用這個函數(shù),暫不做深入介紹。
二、發(fā)送信號
1. 如何發(fā)送信號
進(jìn)程可以通過調(diào)用 kill 函數(shù)向包括它本身在內(nèi)的其他進(jìn)程發(fā)送一個信號。
kill():
$ man 2 kill
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
把參數(shù) sig 給指定的信號發(fā)送給由參數(shù) pid 指定的進(jìn)程號所指定的進(jìn)程。
kill 調(diào)用會在失敗時返回 -1 并設(shè)置 errno 變量,失敗的原因:
-
給定的信號無效(errno設(shè)置為EINVAL);
-
發(fā)送進(jìn)程權(quán)限不夠(errno設(shè)置為EPERM);
-
目標(biāo)進(jìn)程不存在(errno設(shè)置為ESRCH);
關(guān)于權(quán)限:
要想發(fā)送一個信號,發(fā)送進(jìn)程必須擁有相應(yīng)的權(quán)限,包括2種情況:
-
兩個進(jìn)程必須擁有相同的用戶 ID,即你只能發(fā)送信號給屬于自己的進(jìn)程;
-
超級用戶可以發(fā)送信號給任何進(jìn)程;
2. 鬧鐘功能
進(jìn)程可以通過調(diào)用 alarm() 函數(shù)在經(jīng)過預(yù)定時間后發(fā)送一個 SIGALRM 信號。
alarm():
$ man 2 alarm
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
-
在 seconds 秒之后發(fā)送一個 SIGALRM 信號。
-
返回值是以前設(shè)置的鬧鐘時間的余留秒數(shù),如果調(diào)用失敗則返回 -1。
相關(guān)要點(diǎn):
-
由于處理的延時和時間調(diào)度的不確定性,實(shí)際鬧鐘時間將比預(yù)先安排的要稍微拖后一點(diǎn)兒。
-
把參數(shù) seconds 設(shè)置為 0 將取消所有已設(shè)置的鬧鐘請求。
-
如果在接收到 SIGALRM 信號之前再次調(diào)用 alarm() 函數(shù),則鬧鐘重新開始計時
-
每個進(jìn)程只能有一個鬧鐘時間。
3. 入門實(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;
}
通過 fork 調(diào)用啟動新的進(jìn)程:子進(jìn)程休眠 5 秒后向其父進(jìn)程發(fā)送一個 SIGALRM 信號。父進(jìn)程在安排好捕獲 SIGALRM 信號后暫停運(yùn)行,直到接收到一個信號為止。
運(yùn)行效果:
$ ./alarm
alarm application starting
waiting for alarm to go off
<等待5 秒鐘>
Ding!
done
相關(guān)要點(diǎn):
-
pause() 把程序的執(zhí)行掛起直到有一個信號出現(xiàn)為止。使用信號并掛起程序的執(zhí)行是 Unix 程序設(shè)計中的一個重要部分。
$ man 2 pause
#include <unistd.h>
int pause(void); -
當(dāng)它被一個信號中斷時,將返回 -1(如果下一個接收到的信號沒有導(dǎo)致程序終止的話)并把 errno 設(shè)置為 EINTR。
-
更常見的方法是使用 sigsuspend() 函數(shù),暫不做介紹。
-
在信號處理函數(shù)中沒有調(diào)用 printf,而是通過設(shè)置標(biāo)志,然后在main函數(shù)中檢查該標(biāo)志來完成消息的輸出。
-
如果信號出現(xiàn)在系統(tǒng)調(diào)用的執(zhí)行過程中會怎么樣?
-
一般只需要考慮“慢”系統(tǒng)調(diào)用,例如從終端讀數(shù)據(jù),如果在這個系統(tǒng)調(diào)用等待數(shù)據(jù)時出現(xiàn)一個信號,它就會返回錯誤 EINTR。 $ man 3 errno
EINTR
Interrupted function call (POSIX.1); see signal(7). -
如果你開始在自己的程序中使用信號,就需要注意一些系統(tǒng)調(diào)用會因?yàn)榻邮盏搅艘粋€信號而失敗。
-
我們需要更健壯的信號接口:
-
在編寫程序中處理信號部分的代碼時必須非常小心,因?yàn)樵谑褂眯盘柕某绦蛑袝霈F(xiàn)各種各樣的“競態(tài)條件”。例如,如果想調(diào)用pause等待一個信號,可信號卻出現(xiàn)在調(diào)用 pause() 之前,就會使程序無限期地等待一個不會發(fā)生的事件。
-
POSIX 標(biāo)準(zhǔn)推薦了一個更新和更健壯的信號編程接口:sigaction。
三、信號集 (Signal Set)
多個信號可使用一個稱之為信號集的數(shù)據(jù)結(jié)構(gòu)來表示,POSIX.1 定義了數(shù)據(jù)類型 sigset_t 以表示一個信號集,并且定義了下列 5 個處理信號集的函數(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 指向的信號集,清除其中所有信號。
-
函數(shù) sigfillset() 初始化由參數(shù) set 指向的信號集,使其包括所有信號。
-
必須使用 sigemptyset() 或者 sigfillset() 來初始化信號集。這是因?yàn)?C 語言不會對自動變量進(jìn)行初始化,并且,借助于將靜態(tài)變量初始化為 0 的機(jī)制來表示空信號集的作法在可移植性上存在問題,因?yàn)橛锌赡苁褂梦谎诖a之外的結(jié)構(gòu)來實(shí)現(xiàn)信號集。
-
函數(shù) sigaddset() 將一個信號添加到已有的信號集中,sigdelset() 則從信號集中刪除一個信號。
-
sigismember() 函數(shù)用來測試信號 sig 是否是信號集 set 的成員。
四、信號屏蔽字 (Signal Mask)
4.1 基礎(chǔ)概念
每個進(jìn)程都有一個信號屏蔽字(或稱信號掩碼,signal mask),它規(guī)定了當(dāng)前要阻塞遞送到該進(jìn)程的信號集。對于每種信號,屏蔽字中都有一位與之對應(yīng)。對于某種信號,若其對應(yīng)位被設(shè)置,則它當(dāng)前是被阻塞的。進(jìn)程可以調(diào)用 sigprocmask() 檢測或更改,或同時進(jìn)行檢測和更改進(jìn)程的信號屏蔽字。
向信號屏蔽字中添加信號的3種方式:
-
當(dāng)調(diào)用信號處理器 (signal handler) 時,可能會引發(fā)信號自動添加到信號屏蔽字中的行為,暫不作深入介紹。
-
使用 sigaction() 函數(shù)建立信號處理器時,可以指定一組信號集,當(dāng)調(diào)用該處理器時會將該信號集里的信號阻塞,暫不作深入介紹。
-
使用sigprocmask()系統(tǒng)調(diào)用,可以隨時顯式地向信號屏蔽字中添加或移除信號。
先來了解 sigprocmask():
$ man 2 sigprocmask
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
相關(guān)知識點(diǎn):
-
sigprocmask() 既可用于修改 進(jìn)程的信號屏蔽字,也可用于獲取現(xiàn)有的屏蔽字,或者同時執(zhí)行這2個操作。
-
參數(shù) how 指定了 sigprocmask() 該如何操作信號屏蔽字。
-
SIG_BLOCK: 將參數(shù) set 信號集內(nèi)的信號添加到信號屏蔽字中; -
SIG_UNBLOCK: 將參數(shù) set 信號集內(nèi)的信號從信號屏蔽字中移除; -
SIG_SETMASK: 將參數(shù) set 信號集賦給信號屏蔽字。 -
若 set 參數(shù)不為空,則其指向一個 sigset_t 緩沖區(qū),用于返回之前的信號屏蔽字。
-
SUSv3 規(guī)定,如果有任何正在等待的信號 (pending signals) 因調(diào)用了 sigprocmask() 解除了鎖定,那么在此調(diào)用返回前至少會傳遞一次這些信號。
-
系統(tǒng)將忽略試圖阻塞 SIGKILL 和 SIGSTOP 信號的請求。如果試圖阻塞這些信號,sigprocmask() 既不會予以關(guān)注,也不會產(chǎ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> 為所有信號注冊同一個信號處理函數(shù),用于驗(yàn)證信號集是否被成功屏蔽:
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() 是不可靠的,這里為了簡化程序而采用該接口。
2> 初始化信號集,然后屏蔽所有信號:
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);
先屏蔽所有的信號,然后睡眠。睡眠期間,進(jìn)程無法響應(yīng)除 SIGSTOP 和 SIGKILL 之外的任何信號。
3> 睡眠結(jié)束后,用空信號集來解除所有的信號屏蔽:
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);
}
解除了對某個等待信號的屏蔽后,系統(tǒng)會立刻將該信號傳遞一次給進(jìn)程。
打印信號集 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
在信號被屏蔽的 5 秒期間,連續(xù)按下 3 次 ctrl + c,所有信號都不會被處理。當(dāng)過了 5 秒后,解除信號屏蔽,僅僅有一次 SIGINT 信號被成功地傳遞并處理。
五、等待中的信號 (Pending Signals)
如果某進(jìn)程接受了一個該進(jìn)程正在阻塞的信號,那么會將該信號填加到進(jìn)程的等待信號集中。當(dāng)解除對該信號的鎖定時,會隨之將信號傳遞給此進(jìn)程。為了確定進(jìn)程中處于等待狀態(tài)的是哪些信號,可以使用 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)的信號集,并將其置于 set 指向的sigset_t 中。
相關(guān)知識點(diǎn):
-
如果修改了對等待信號的處置 (術(shù)語disposition),那么當(dāng)后來解除對信號的鎖定時,將根據(jù)新的處置來處理信號。
六、待處理的信號 (Pending Signals)
如果某進(jìn)程接受了一個該進(jìn)程正在阻塞 (blocking) 的信號,那么會將該信號填加到進(jìn)程的 等待信號集 (set of pending signals) 中。當(dāng)解除對該信號的阻塞時,會隨之將信號傳遞給此進(jìn)程。可以使用 sigpending() 確定進(jìn)程中處于等待狀態(tài)的是哪些信號。
$ man 2 sigpending
#include <signal.h>
int sigpending(sigset_t *set);
sigpending() 為調(diào)用進(jìn)程返回處于等待狀態(tài)的信號集,并將其置于參數(shù) set 指向的 sigset_t 中。
1. 一個簡單的例子 (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 的信號處理函數(shù); -
屏蔽 SIGQUIT; -
睡眠 5 秒,用于等待 SIGQUIT 信號; -
睡眠結(jié)束,檢測 SIGQUIT 是否處于 pending; -
解除屏蔽 SIGQUIT;
注意:在設(shè)置 SIGQUIT 為阻塞時,我們保存了老的屏蔽字。為了解除對該信號的阻塞,用老的屏蔽字重新設(shè)置了進(jìn)程信號屏蔽字。另一種方法是用 SIG_UNBLOCK 使阻塞的信號不再阻塞。如果編寫一個可能由其他人使用的函數(shù),而且需要在函數(shù)中阻塞一個信號,則不能用 SIG_UNBLOCK 簡單地解除對此信號的阻塞,這是因?yàn)榇撕瘮?shù)的調(diào)用者在調(diào)用本函數(shù)之前可能也阻塞了此信號。
2> 信號處理函數(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 // 在信號處理程序中
SIGQUIT unblocked // 從sigprocmask() 返回
^\Quit (core dumped)
2 個值得注意的點(diǎn):
-
信號處理函數(shù)是在 sigprocmask() unblock 信號返回之前被調(diào)用;
-
用 signal() 設(shè)置信號處理函數(shù),信號被處理時,會將信號處置重置為其默認(rèn)行為。要想在同一信號“再度光臨”時再次調(diào)用該信號處理器函數(shù),程序員必須在信號處理器內(nèi)部調(diào)用signal(),以顯式重建處理器函數(shù),但是這種處理方式是不安全的,真實(shí)的項(xiàng)目里應(yīng)使用 sigaction(),后續(xù)的文章會舉例講解。
七、不對待處理的信號進(jìn)行排隊(duì)處理
等待信號集只是一個掩碼,僅表明一個信號是否發(fā)生,而未表明其發(fā)生的次數(shù)。換言之,如果同一信號在阻塞狀態(tài)下產(chǎn)生多次,那么會將該信號記錄在等待信號集中,并在稍后僅傳遞一次。后面會介紹實(shí)時信號,對實(shí)時信號所采取的是隊(duì)列化管理。如果將某一實(shí)時信號的多個實(shí)例發(fā)送給一進(jìn)程,那么將會多次傳遞該實(shí)時信號,暫不做深入介紹。
1. 仍是那個簡單的例子 (sig_pending.c)
為了降低學(xué)習(xí)難度,跟前面的 Pending Signals 章節(jié)使用同一個例子,修改一下測試步驟:
$ ./sig_pending
^\^\^\ // 按下 3 次 ctrl + \ (在5s之內(nèi))
SIGQUIT pending // 從 sleep(5) 返回后
caught SIGQUIT // 只調(diào)用了一次信號處理程序
SIGQUIT unblocked // 從sigprocmask() 返回
^\Quit (core dumped)
第二次運(yùn)行該程序時,在進(jìn)程休眠期間產(chǎn)生了 3 次 SIGQUIT 信號,但是取消對該信號的阻塞后,系統(tǒng)只向進(jìn)程傳送了一次 SIGQUIT,從中可以看出在 Linux 系統(tǒng)上沒有對信號進(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 來描述一個進(jìn)程,struct task_struct 中信號相關(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 建立了一個鏈表,該鏈表包含了所有已經(jīng)產(chǎn)生、且有待內(nèi)核處理的信號,其定義如下:
struct sigpending {
struct list_head list;
sigset_t signal;
};
-
成員 struct list_head list 通過雙向鏈表管理所有待處理信號,每一種待處理的信號對應(yīng)雙向鏈表中的 1 個 struct sigqueue 節(jié)點(diǎn)。
-
成員 sigset_t signal 是位圖 (bit mask,或稱位掩碼),它指定了仍然有待處理的所有信號的編號。某 1 bit = 1 表示該 bit 對應(yīng)的信號待處理。sigset_t 所包含的比特位數(shù)目要 >= 所支持的信號數(shù)目。因此,內(nèi)核使用了 unsigned long 數(shù)組來定義該數(shù)據(jù)類型:
typedef struct {
unsigned long sig[_NSIG_WORDS];
} sigset_t;
-
struct sigqueue 的定義如下:
struct sigqueue {
struct list_head list;
int flags;
siginfo_t info;
...
};
-
siginfo_t 用于保存信號的額外信息,暫時不用關(guān)心。
注意:在 struct sigpending 鏈表中,struct sigqueue 對應(yīng)的是一種類型的待處理信號,而不是某一個具體的信號。
示意圖:
2) 信號的產(chǎn)生
當(dāng)給進(jìn)程發(fā)送一個信號時,這個信號可能來自內(nèi)核,也可能來自另外一個進(jìn)程。
內(nèi)核里有多個 API 能產(chǎn)生信號,這些 API 最終都會調(diào)用 send_signal()。我們重點(diǎn)關(guān)注信號是何時被設(shè)置為 pending 狀態(tài)的。
linux/kernel/signal.c:
send_signal()
__send_signal()
struct sigqueue *q = __sigqueue_alloc();
list_add_tail(&q->list, &pending->list); // 將待處理信號添加到 pending 鏈表中
sigaddset(&pending->signal, sig); // 在位圖中將信號對應(yīng)的 bit 置 1
complete_signal(sig, t, group);
signal_wake_up();
send_signal() 會分配一個新的 struct sigqueue 實(shí)例,然后為其填充信號的額外信息,并添加到目標(biāo)進(jìn)程的 sigpending 鏈表且設(shè)置位圖。
如果信號成功發(fā)送,沒有被阻塞,就可以用 signal_wake_up() 喚醒目標(biāo)進(jìn)程,使得調(diào)度器可以選擇目標(biāo)進(jìn)程運(yùn)行。
3) 信號的傳遞:
這些知識放在這篇文章里已經(jīng)完全超綱了,如果將所有的細(xì)節(jié)都暴露出來會讓初學(xué)者感到極度的困惑。
所以,我們只邁出一小步,將僅剩的一點(diǎn)注意力集中在內(nèi)核在執(zhí)行信號處理函數(shù)前是如何處理 pending 信號的。
在每次由內(nèi)核態(tài)切換到用戶態(tài)時,內(nèi)核都會進(jìn)行信號處理,最終的效果就是調(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); // 取消信號的 pending 狀態(tài)
list_del_init(&first->list); // 刪除 pending 鏈表中的 struct sigqueue 節(jié)點(diǎn)
copy_siginfo(info, &first->info);
-
handle_signal() 會操作進(jìn)程在用戶態(tài)下的棧,使得在從內(nèi)核態(tài)切換到用戶態(tài)之后運(yùn)行信號處理程序,而不是正常的程序代碼。
-
do_signal() 返回時,信號處理函數(shù)就會被執(zhí)行。
七、相關(guān)參考
-
《Unix 環(huán)境高級編程-第10章 信號》 -
《Linux/Unix 系統(tǒng)編程手冊-第20章 信號:基本概念》 -
《Linux 系統(tǒng)編程-第10章 信號》 -
《Linux 程序設(shè)計-第11章 進(jìn)程和信號》 -
《深入理解 Linux 內(nèi)核 第11章 信號》 -
《深入 Linux 內(nèi)核架構(gòu) 5.4.1信號》 -
《Linux 內(nèi)核源代碼情景分析 6.4信號》
你和我各有一個蘋果,如果我們交換蘋果的話,我們還是只有一個蘋果。但當(dāng)你和我各有一個想法,我們交換想法的話,我們就都有兩個想法了。如果你也對 嵌入式系統(tǒng)和開源軟件 感興趣,并且想和更多人互相交流學(xué)習(xí)的話,請關(guān)注我的公眾號:嵌入式系統(tǒng)磚家,一起來學(xué)習(xí)吧,無論是 關(guān)注或轉(zhuǎn)發(fā) ,還是賞賜,都是對作者莫大的支持,謝謝 各位的大拇指 ,祝工作順利,家庭和睦~
-END-
推薦閱讀
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點(diǎn),不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!