起點(diǎn)網(wǎng)站建設(shè)百度電話客服24小時(shí)人工服務(wù)熱線
信號是 Linux 操作系統(tǒng)中非常重要的進(jìn)程控制機(jī)制,用來異步通知進(jìn)程發(fā)生某種事件。理解信號的產(chǎn)生、阻塞、遞達(dá)、捕捉等概念,可以幫助開發(fā)者更好地編寫健壯的應(yīng)用程序,避免由于未處理的信號導(dǎo)致程序異常退出。本文將帶你從基礎(chǔ)概念開始,深入探討信號處理的常見方式。
1. 信號的基本概念
在 Linux 系統(tǒng)中,信號是一種軟中斷機(jī)制,主要用于進(jìn)程之間的異步通信。信號的產(chǎn)生和遞達(dá)并不會(huì)按照進(jìn)程的執(zhí)行順序發(fā)生,而是通過操作系統(tǒng)將某種事件(如用戶輸入、硬件異常等)通知進(jìn)程。每個(gè)信號都有唯一的編號和宏定義名稱。例如,SIGINT
(編號2)是一個(gè)常見的信號,通常由按下 Ctrl+C
產(chǎn)生。
信號的產(chǎn)生的四種方式:
- 終端輸入產(chǎn)生信號:例如,當(dāng)用戶在終端中按下
Ctrl+C
時(shí),系統(tǒng)會(huì)發(fā)送SIGINT
信號給當(dāng)前前臺(tái)進(jìn)程,進(jìn)而終止進(jìn)程。- 系統(tǒng)調(diào)用產(chǎn)生信號:開發(fā)者可以通過調(diào)用
kill
函數(shù)向指定進(jìn)程發(fā)送信號,例如kill -SIGKILL <PID>
。- 軟件條件產(chǎn)生信號:某些軟件事件會(huì)自動(dòng)觸發(fā)信號。例如,
SIGPIPE
信號在管道破裂時(shí)觸發(fā)。- 硬件異常產(chǎn)生信號:例如,執(zhí)行非法內(nèi)存訪問會(huì)觸發(fā)
SIGSEGV
信號。
信號處理一般有三種方式:
- 忽略信號:進(jìn)程忽略該信號。
- 執(zhí)行默認(rèn)動(dòng)作:進(jìn)程按照信號的默認(rèn)處理方式處理,例如
SIGKILL
會(huì)導(dǎo)致進(jìn)程直接退出。- 捕捉信號:進(jìn)程通過自定義函數(shù)捕捉信號并進(jìn)行處理。
2. 信號的自定義捕捉
Linux 提供了 signal
和 sigaction
系統(tǒng)調(diào)用,允許開發(fā)者自定義信號的處理函數(shù),即捕捉信號。
a. 自定義捕捉終端產(chǎn)生的 SIGINT 2號信號:
#include <stdio.h>
#include <signal.h>void handler(int sig) {printf("Received signal: %d\n", sig);
}int main() {signal(SIGINT, handler); // 捕捉SIGINT信號while (1) {printf("Waiting for signal...\n");sleep(1);}return 0;
}
程序解釋
handler
函數(shù)捕捉到 SIGINT
信號并打印信號編號。當(dāng)用戶在終端中按下 Ctrl+C
,進(jìn)程不會(huì)立即終止,而是調(diào)用自定義的 handler
函數(shù)。
b. 自定義捕捉由軟件條件產(chǎn)生的 SIGALRM 信號
SIGALRM
信號通常由 alarm()
函數(shù)產(chǎn)生,用于在設(shè)定的時(shí)間后通知進(jìn)程。
alarm()
函數(shù)與 SIGALRM
信號
alarm()
函數(shù)的作用是設(shè)置一個(gè)鬧鐘,指定經(jīng)過若干秒后系統(tǒng)向進(jìn)程發(fā)送 SIGALRM
信號。此信號的默認(rèn)處理行為是終止進(jìn)程,但我們可以通過自定義信號處理函數(shù)來捕捉并處理 SIGALRM
信號。
alarm()
函數(shù)的定義如下:
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
- 參數(shù)
seconds
:指定在多少秒后發(fā)送SIGALRM
信號。如果參數(shù)為 0,則取消先前設(shè)置的鬧鐘。 - 返回值:返回先前設(shè)置的鬧鐘還剩余的時(shí)間。如果沒有設(shè)置過鬧鐘,返回值為 0。例如,設(shè)定鬧鐘為 30 秒,鬧鐘執(zhí)行了 20 秒后取消并重設(shè)為 15 秒,之前的鬧鐘還剩下 10 秒,這個(gè)時(shí)間會(huì)作為
alarm()
函數(shù)的返回值。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>// 自定義的SIGALRM信號處理函數(shù)
void handle_alarm(int sig) {printf("Alarm signal received: %d. Time's up!\n", sig);
}int main() {// 注冊SIGALRM信號的處理函數(shù)signal(SIGALRM, handle_alarm);// 設(shè)置鬧鐘為1秒alarm(1);// 計(jì)數(shù)器,在鬧鐘響起之前一直計(jì)數(shù)int count = 0;while (1) {printf("Counting: %d\n", count++);sleep(1); // 每秒計(jì)數(shù)一次}return 0;
}
程序解釋
-
信號處理函數(shù)
handle_alarm
:
當(dāng)SIGALRM
信號產(chǎn)生時(shí),操作系統(tǒng)會(huì)調(diào)用此函數(shù)。此函數(shù)接收信號編號作為參數(shù),并打印出提示信息。 -
設(shè)置鬧鐘:
程序通過調(diào)用alarm(1)
設(shè)置了一個(gè) 1 秒的鬧鐘。即 1 秒后,系統(tǒng)會(huì)發(fā)送SIGALRM
信號,觸發(fā)信號處理函數(shù)handle_alarm
。 -
計(jì)數(shù)器:
程序進(jìn)入一個(gè)無限循環(huán),每秒鐘打印一次計(jì)數(shù)。在 1 秒內(nèi),計(jì)數(shù)器會(huì)輸出幾次,直到接收到SIGALRM
信號并終止循環(huán)。
運(yùn)行該程序時(shí),輸出:
Counting: 0
Alarm signal received: 14. Time's up!
程序開始計(jì)數(shù),并且在 1 秒后接收到 SIGALRM
信號,調(diào)用信號處理函數(shù),輸出“Alarm signal received”信息,隨后程序被信號終止。
拓展
-
重新設(shè)置鬧鐘:如果在
SIGALRM
之前再次調(diào)用alarm()
,會(huì)取消之前的鬧鐘并重新設(shè)置。例如,假如alarm(5)
在alarm(1)
之后被調(diào)用,系統(tǒng)將在 5 秒而非 1 秒后發(fā)送SIGALRM
信號。 -
取消鬧鐘:
alarm(0)
可以取消先前設(shè)置的鬧鐘,程序?qū)⒉粫?huì)再收到SIGALRM
信號。
3. 信號阻塞與未決信號
信號的三種狀態(tài):阻塞、未決和遞達(dá)
阻塞(Block):
阻塞是指進(jìn)程可以暫時(shí)不處理某些信號。當(dāng)信號被阻塞時(shí),即使信號產(chǎn)生了,也不會(huì)立即處理。信號會(huì)保持在阻塞狀態(tài),直到解除阻塞。
未決(Pending):
未決是指信號已經(jīng)產(chǎn)生,但由于被阻塞,無法遞達(dá)。信號會(huì)處于未決狀態(tài),等待解除阻塞。當(dāng)信號解除阻塞后,未決信號會(huì)遞達(dá)給進(jìn)程。
遞達(dá)(Delivery):
遞達(dá)是指信號從產(chǎn)生到被進(jìn)程處理的過程。當(dāng)信號未被阻塞或解除阻塞后,信號會(huì)遞達(dá)給進(jìn)程,觸發(fā)默認(rèn)處理動(dòng)作或自定義的信號處理函數(shù)。
進(jìn)程可以選擇阻塞(Block)某個(gè)信號。被阻塞的信號在產(chǎn)生時(shí)會(huì)處于未決狀態(tài),直到進(jìn)程解除對該信號的阻塞后,才會(huì)執(zhí)行相關(guān)的處理動(dòng)作。
注意:阻塞和忽略是不同的概念。阻塞信號意味著信號不會(huì)被遞達(dá),直到解除阻塞。而忽略則是在信號遞達(dá)后選擇不進(jìn)行處理的一種方式。
C語言中的信號相關(guān)函數(shù)
sigset_t
是一個(gè)用于表示信號集的數(shù)據(jù)類型,每個(gè)信號用一個(gè)位來表示它的狀態(tài)。這個(gè)類型可以用來存儲(chǔ)和操作進(jìn)程的信號屏蔽字(阻塞信號集),以及未決信號集。
-
sigemptyset(sigset_t *set)
: 初始化一個(gè)信號集,將其中的所有信號位清零,即該信號集不包含任何信號。 -
sigfillset(sigset_t *set)
: 初始化一個(gè)信號集,將其中的所有信號位設(shè)置為1,即該信號集包含所有信號。 -
sigaddset(sigset_t *set, int signo)
: 在信號集中添加一個(gè)信號,使其對應(yīng)的位被設(shè)置為1。signo
是要添加的信號的編號。 -
sigdelset(sigset_t *set, int signo)
: 從信號集中刪除一個(gè)信號,使其對應(yīng)的位被設(shè)置為0。signo
是要?jiǎng)h除的信號的編號。 -
sigismember(const sigset_t *set, int signo)
: 檢查信號集中的某個(gè)信號是否被設(shè)置為1。返回值為非零表示信號被設(shè)置(即有效),為0表示信號未被設(shè)置(即無效)。 -
sigprocmask(int how, const sigset_t *set, sigset_t *oset)
: 讀取或更改進(jìn)程的信號屏蔽字(阻塞信號集)。 -
sigpending(sigset_t *set)
: 讀取當(dāng)前進(jìn)程的未決信號集。
使用 sigprocmask
添加block阻塞集
在某些場景下,進(jìn)程可能不希望立即處理某個(gè)信號,這時(shí)可以選擇阻塞該信號。當(dāng)信號被阻塞時(shí),信號會(huì)進(jìn)入未決狀態(tài),直到解除阻塞后才會(huì)遞達(dá)。可以使用 sigprocmask
函數(shù)來設(shè)置信號的阻塞狀態(tài)。
#include <stdio.h>
#include <signal.h>int main() {sigset_t set;sigemptyset(&set);sigaddset(&set, SIGINT); // 阻塞SIGINT信號sigprocmask(SIG_BLOCK, &set, NULL);printf("SIGINT is blocked, press Ctrl+C...\n");sleep(10); // 期間按下Ctrl+C不會(huì)終止程序return 0;
}
SIGINT
信號被阻塞,按下 Ctrl+C
時(shí),進(jìn)程不會(huì)立刻退出,而是被阻塞,不能被遞達(dá),需要等到信號被解除阻塞后才能處理信號。
如果將所有信號都使用sigprocmask函數(shù)添加到block阻塞位圖當(dāng)中,是不是就能產(chǎn)生一個(gè)無法被退出的無敵進(jìn)程?
答:SIGKILL (9號信號)和 SIGSTOP(19號信號)是特殊的,無法被阻塞、忽略或捕獲。我們依然可以通過這些信號殺死進(jìn)程,所以這樣操作并不能使進(jìn)程成為“無敵”的進(jìn)程。(操作系統(tǒng)設(shè)計(jì)者早就想到了~~)
4. 常見信號的遞達(dá)過程和處理方式
信號的遞達(dá)過程
-
信號抵達(dá)的檢查: 當(dāng)系統(tǒng)從內(nèi)核態(tài)返回用戶態(tài)時(shí),會(huì)首先檢查當(dāng)前進(jìn)程的未決信號(pending signals)。這時(shí),系統(tǒng)處于內(nèi)核態(tài),有權(quán)限檢查進(jìn)程的信號狀態(tài)。
-
處理未決信號:
- 如果發(fā)現(xiàn)有未決信號,并且該信號沒有被阻塞,系統(tǒng)會(huì)決定如何處理這些信號。
- 對于默認(rèn)處理動(dòng)作或忽略的信號,系統(tǒng)會(huì)執(zhí)行默認(rèn)動(dòng)作或忽略信號,然后清除對應(yīng)的未決標(biāo)志位。
-
指定信號動(dòng)作:
- 如果信號的處理動(dòng)作是用戶自定義的,系統(tǒng)會(huì)返回用戶態(tài),執(zhí)行用戶定義的處理函數(shù)。執(zhí)行完自定義處理函數(shù)后,用戶態(tài)的處理程序會(huì)通過
sigreturn
系統(tǒng)調(diào)用返回內(nèi)核態(tài),清除對應(yīng)的未決標(biāo)志位。如果沒有新的信號要處理,系統(tǒng)會(huì)直接返回用戶態(tài),從主控制流程中上次被中斷的地方繼續(xù)執(zhí)行。
- 如果信號的處理動(dòng)作是用戶自定義的,系統(tǒng)會(huì)返回用戶態(tài),執(zhí)行用戶定義的處理函數(shù)。執(zhí)行完自定義處理函數(shù)后,用戶態(tài)的處理程序會(huì)通過
-
為什么執(zhí)行自定義函數(shù)是需要由內(nèi)核態(tài)切換到內(nèi)核態(tài):
- 盡管內(nèi)核態(tài)具有高權(quán)限,但操作系統(tǒng)設(shè)計(jì)中不允許直接在內(nèi)核態(tài)執(zhí)行用戶代碼。原因是用戶代碼可能包含非法操作,如清空數(shù)據(jù)庫等,這在用戶態(tài)時(shí)權(quán)限不足,但在內(nèi)核態(tài)時(shí)可能會(huì)造成嚴(yán)重后果。
- 操作系統(tǒng)必須確保用戶代碼的合法性,以防止安全風(fēng)險(xiǎn)。因此,操作系統(tǒng)會(huì)嚴(yán)格控制用戶代碼的執(zhí)行,確保系統(tǒng)安全和穩(wěn)定。
信號的處理方式
信號編號 | 宏定義名稱 | 默認(rèn)動(dòng)作 | 說明 |
---|---|---|---|
1 | SIGHUP | 終止進(jìn)程 | 終端掛起時(shí)發(fā)送此信號 |
2 | SIGINT | 終止進(jìn)程 | 用戶按下 Ctrl+C |
9 | SIGKILL | 終止進(jìn)程(不可捕捉) | 直接終止進(jìn)程 |
11 | SIGSEGV | 終止進(jìn)程并生成 core | 段錯(cuò)誤,非法內(nèi)存訪問 |
15 | SIGTERM | 終止進(jìn)程 | 請求進(jìn)程終止 |