一、Linux 信號基本概念
1.1 生活角度理解信號
我們可以把進程比作等待快遞的人,信號就像快遞:
- 識別信號:就像我們知道快遞來了該?怎么處理,進程對信號的識別是內核程序員預先編寫的內置特性,即使信號沒產生,進程也知道該如何處理。
- 信號延遲處理:比如正在打游戲時收到快遞通知,會等游戲結束再去取,進程收到信號后,若在執行優先級更高的任務,會在合適的時候處理信號。
- 信號記錄:從收到快遞通知到取到快遞的這段時間,我們會記住有快遞要取,進程收到信號后,在未處理前也會暫時記錄信號。
- 信號處理方式:處理快遞有打開使用(默認動作)、送給他人(自定義動作)、扔在一邊(忽略)三種方式,進程處理信號也有默認、自定義、忽略三種方式,自定義處理信號也叫信號捕捉。
1.2 技術角度理解信號
信號是進程之間事件異步通知的一種方式,屬于軟中斷。比如在 Shell 下啟動一個前臺進程,當我們按下Ctrl+C
,會產生一個硬件中斷,被操作系統獲取后解釋成SIGINT
(2 號信號)發送給前臺進程,前臺進程收到信號后會退出,這就是信號的實際應用。
1.3 查看信號
在 Linux 系統中,我們可以通過相關命令查看信號,每個信號都有對應的編號和宏定義名稱,這些宏定義可在signal.h
中找到。例如:
SIGINT
(2 號信號):來自鍵盤的中斷信號。SIGQUIT
(3 號信號):來自鍵盤的退出信號,會生成 core dump 文件。SIGKILL
(9 號信號):殺死進程的信號,無法被捕捉和忽略。
我們也可以通過man 7 signal
命令查看每個信號的產生條件和默認處理動作,部分常見信號如下表:
Signal | Standard | Action | Comment |
---|---|---|---|
SIGABRT | P1990 | Core | 來自 abort (3) 的中止信號 |
SIGALRM | P1990 | Term | 來自 alarm (2) 的定時器信號 |
SIGBUS | P2001 | Core | 總線錯誤(不良內存訪問) |
SIGCHLD | P1990 | Ign | 子進程停止或終止 |
SIGINT | P1990 | Term | 來自鍵盤的中斷 |
二、信號產生的一般方式
2.1 通過終端按鍵產生信號
- Ctrl+C(SIGINT,2 號信號):發送中斷信號,默認終止前臺進程。
- 示例:編寫一個簡單的循環程序,運行后按下
Ctrl+C
,進程會退出。
- 示例:編寫一個簡單的循環程序,運行后按下
#include <iostream>
#include <unistd.h>
int main() {while (true) {std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}return 0;
}
編譯運行:g++ sig.cc -o sig && ./sig
,按下Ctrl+C
,進程終止
- Ctrl+\(SIGQUIT,3 號信號):發送退出信號,默認終止進程并生成 core dump 文件,用于事后調試。
- 示例:修改上述程序,捕捉
SIGQUIT
信號。
- 示例:修改上述程序,捕捉
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber) {std::cout << "我是: " << getpid() << ", 我獲得了一個信號: " << signumber << std::endl;
}
int main() {std::cout << "我是進程: " << getpid() << std::endl;signal(SIGQUIT/*3*/, handler);while (true) {std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}return 0;
}
編譯運行后,按下Ctrl+\
,會輸出信號編號,若注釋掉信號捕捉代碼,按下Ctrl+\
,進程會退出并生成 core dump 文件。
- Ctrl+Z(SIGTSTP,20 號信號):發送停止信號,默認將當前前臺進程掛起到后臺。
- 示例:運行上述未捕捉
SIGTSTP
信號的循環程序,按下Ctrl+Z
,進程會被掛起,使用jobs
命令可查看后臺進程,使用fg
命令可將后臺進程調回前臺。
2.1.2? 實操:使用signal函數自定義SIGINT信號的處理方式
以下是一個小實驗,大家可以練下手加深理解
signal
?函數用于設置信號的處理方式,它的原型是:
#include <signal.h>
void (*signal(int signum, void (*handler)(int)))(int);
signum
:要設置處理方式的信號編號,比如?SIGINT
。handler
:指向信號處理函數的指針。信號處理函數的原型是?void handler(int signum)
,其中?signum
?是接收到的信號編號。
代碼解釋
首先,我們包含了必要的頭文件:
stdio.h
(用于輸入輸出)、signal.h
(用于信號相關操作)、unistd.h
(用于?sleep
?函數)。定義了自定義的信號處理函數?
sigcb
,它接收一個?int
?類型的參數?signum
(即接收到的信號編號),在函數內部打印出接收到的信號值。在?
main
?函數中,使用?signal(SIGINT, sigcb)
?來設置?SIGINT
?信號的處理函數為?sigcb
。這樣,當進程接收到?SIGINT
?信號時,就會調用?sigcb
?函數而不是默認的終止進程操作。然后打印提示信息,接著通過一個無限循環?
while (1)
?讓程序保持運行,sleep(1)
?是為了避免程序過度占用 CPU。編譯與運行
編譯代碼:使用?
gcc
?編譯器,在終端中輸入命令?gcc -o sig_demo sig_demo.c
(假設代碼文件名為?sig_demo.c
)。運行程序:在終端中輸入?
./sig_demo
,程序會開始運行并打印提示信息。測試信號:按下?
Ctrl + C
,此時會觸發?SIGINT
?信號,程序會調用?sigcb
?函數,打印出類似?接收到信號,信號值為:2
(SIGINT
?的值通常為 2)的信息,而不是終止程序。執行結果截圖說明
編譯運行后,終端會顯示 “程序正在運行,按下ctrl+c發送SIGINT信號”。
當按下?
Ctrl + C
?時,終端會打印 “接收到信號為:2”,然后程序繼續運行(因為我們的處理函數沒有終止進程,而是讓程序繼續在循環中運行)。如果多次按下?Ctrl + C
,會多次打印該信息。
SIGINT
(Ctrl+C)被我們自定義處理了,但系統還有其他信號可以終止進程,比如?SIGQUIT
(通常由?Ctrl+\
?觸發)。
通過這個例子,你可以清楚地看到如何使用?signal
?函數來自定義信號的處理方式,以及?SIGINT
?信號的觸發和處理過程。
2.1.3? 實操2:使用sigaction函數自定義SIGINT信號的處理方式
sigaction
?函數是比?signal
?函數更強大、更可移植的信號處理接口。它可以更精細地控制信號的處理行為。sigaction
?函數的原型如下:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
signum
:要設置處理方式的信號編號,比如?SIGINT
。act
:指向?struct sigaction
?結構體的指針,該結構體包含了新的信號處理方式等信息。oldact
:指向?struct sigaction
?結構體的指針,用于保存原來的信號處理方式(可以為?NULL
,表示不保存)。
struct sigaction
?結構體的定義大致如下:
struct sigaction {void (*sa_handler)(int);sigset_t sa_mask;int sa_flags;void (*sa_sigaction)(int, siginfo_t *, void *);
};
sa_handler
:指向信號處理函數的指針,和?signal
?函數中的處理函數類似,處理函數原型為?void handler(int signum)
。sa_mask
:指定在信號處理函數執行期間,需要阻塞的信號集合。sa_flags
:用于設置信號處理的一些標志,比如?SA_SIGINFO
?等。sa_sigaction
:當?sa_flags
?中設置了?SA_SIGINFO
?標志時,使用該函數指針所指向的函數來處理信號,這個函數可以獲取更多關于信號的信息,原型為?void handler(int signum, siginfo_t *info, void *context)
。
代碼實現
我們編寫一個 C 程序,使用?sigaction
?函數來自定義?SIGINT
?信號的處理方式:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>// 自定義的信號處理函數
void sigcb(int signum) {printf("接收到信號,信號值為:%d\n", signum);
}int main() {struct sigaction act;// 設置信號處理函數為 sigcbact.sa_handler = sigcb;// 清空 sa_mask,即處理信號期間不阻塞其他信號sigemptyset(&act.sa_mask);// 設置 sa_flags 為 0,使用默認的標志act.sa_flags = 0;// 使用 sigaction 函數設置 SIGINT 信號的處理方式sigaction(SIGINT, &act, NULL);printf("程序正在運行,按下 Ctrl + C 發送 SIGINT 信號\n");// 讓程序保持運行,以便我們可以發送信號while (1) {sleep(1);}return 0;
}
代碼解釋
首先包含必要的頭文件:
stdio.h
(輸入輸出)、signal.h
(信號相關操作)、unistd.h
(sleep
?函數)。定義自定義的信號處理函數?
sigcb
,功能是打印接收到的信號值。在?
main
?函數中,定義?struct sigaction
?類型的變量?act
。設置?
act
?的?sa_handler
?成員為我們自定義的處理函數?sigcb
。使用?
sigemptyset
?函數清空?sa_mask
?成員,這樣在處理?SIGINT
?信號期間,不會阻塞其他信號。將?
sa_flags
?成員設置為?0
,使用默認的標志。調用?
sigaction
?函數,將?SIGINT
?信號的處理方式設置為?act
?所指定的方式,第三個參數為?NULL
,表示不保存原來的信號處理方式。打印提示信息后,通過無限循環讓程序保持運行,
sleep(1)
?避免程序過度占用 CPU。編譯與運行
編譯代碼:使用?
gcc
?編譯器,在終端中輸入命令?gcc -o sigaction_demo sigaction_demo.c
(假設代碼文件名為?sigaction_demo.c
)。運行程序:在終端中輸入?
./sigaction_demo
,程序開始運行并打印提示信息。測試信號:按下?
Ctrl + C
,觸發?SIGINT
?信號,程序會調用?sigcb
?函數,打印出類似?接收到信號,信號值為:2
(SIGINT
?的值通常為 2)的信息,而不是終止程序。執行結果截圖說明
編譯運行后,終端會顯示 “程序正在運行,按下 Ctrl + C 發送 SIGINT 信號”。
當按下?
Ctrl + C
?時,終端會打印 “接收到信號,信號值為:2”,然后程序繼續運行(因為我們的處理函數沒有終止進程,程序在循環中繼續運行)。多次按下?Ctrl + C
,會多次打印該信息。
通過這個例子,你可以掌握?sigaction
?函數的使用方法,以及如何更精細地控制信號的處理方式。
2.2 調用系統命令向進程發信號
我們可以使用kill
命令向指定進程發送信號,例如:
- 后臺運行一個死循環程序:
./sig &
(sig
為上述編譯生成的可執行文件)。- 查看進程 ID:
ps ajx | grep sig
,獲取進程的 PID。- 向進程發送
SIGSEGV
(11 號信號,段錯誤信號):kill -SIGSEGV PID
(或kill -11 PID
),進程會因段錯誤終止。
2.3 使用函數產生信號
2.3.1 kill 函數
kill
函數可以給一個指定的進程發送指定的信號,函數原型如下:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
- 參數說明:
pid
:目標進程的 PID。sig
:要發送的信號編號。
- 返回值:成功返回 0,失敗返回 - 1,并設置
errno
。 - 示例:實現自己的 kill 命令
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
// mykill -signumber pid
int main(int argc, char *argv[]) {if (argc != 3) {std::cerr << "Usage: " << argv[0] << " -signumber pid" << std::endl;return 1;}int number = std::stoi(argv[1] + 1); // 去掉“-”pid_t pid = std::stoi(argv[2]);int n = kill(pid, number);return n;
}
編譯生成mykill
后,可像kill
命令一樣使用,例如./mykill -2 PID
,向指定 PID 的進程發送 2 號信號。
2.3.2 raise 函數
raise
函數可以給當前進程發送指定的信號(自己給自己發信號),函數原型如下:
#include <signal.h>
int raise(int sig);
- 參數說明:
sig
:要發送的信號編號。 - 返回值:成功返回 0,失敗返回非 0。
- 示例
#include <iostream> #include <unistd.h> #include <signal.h> void handler(int signumber) {// 整個代碼就只有這一處打印std::cout << "獲取了一個信號: " << signumber << std::endl; } int main() {signal(2, handler); // 先對2號信號進行捕捉// 每隔1S,自己給自己發送2號信號while (true) {sleep(1);raise(2);}return 0; }
編譯運行后,每隔 1 秒會輸出 “獲取了一個信號: 2”。
2.3.3 abort 函數
abort
函數使當前進程接收到信號而異常終止,函數原型如下:
#include <stdlib.h>
void abort(void);
- 說明:
abort
函數總是會成功,沒有返回值,它會給當前進程發送SIGABRT
(6 號信號)。 - 示例
#include <iostream> #include <unistd.h> #include <stdlib.h> #include <signal.h> void handler(int signumber) {// 整個代碼就只有這一處打印std::cout << "獲取了一個信號: " << signumber << std::endl; } int main() {signal(SIGABRT, handler);while (true) {sleep(1);abort();}return 0; }
編譯運行后,會輸出 “獲取了一個信號: 6”,然后進程異常終止,即使捕捉了SIGABRT
信號,進程也會退出。
2.4 由軟件條件產生信號
以alarm
函數和SIGALRM
(14 號信號)為例,alarm
函數可以設定一個鬧鐘,告訴內核在指定秒數后給當前進程發SIGALRM
信號,該信號的默認處理動作是終止當前進程,函數原型如下:
- 參數說明:
seconds
:鬧鐘時間,單位為秒。若seconds
為 0,表示取消以前設定的鬧鐘。 - 返回值:返回 0 或者以前設定的鬧鐘時間還余下的秒數。
2.4.1 基本 alarm 驗證 - 體會 IO 效率問題
- IO 多的情況
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main() {int count = 0;alarm(1);while (true) {std::cout << "count : " << count << std::endl;count++;}return 0;
}
編譯運行后,1 秒內輸出的計數較少,因為std::cout
是 IO 操作,效率較低。
- IO 少的情況
#include <iostream>
#include <unistd.h>
#include <signal.h>
int count = 0;
void handler(int signumber) {std::cout << "count : " << count << std::endl;exit(0);
}
int main() {signal(SIGALRM, handler);alarm(1);while (true) {count++;}return 0;
}
編譯運行后,1 秒內輸出的計數會非常大,因為僅進行變量自增操作,IO 操作少,效率高。
2.4.2 設置重復鬧鐘
#include <iostream>
#include <unistd.h>
#include <signal.h>
int gcount = 0;
void handler(int signo) {std::cout << "gcount : " << gcount << std::endl;gcount++;int n = alarm(1); // 重設鬧鐘,會返回上一次鬧鐘的剩余時間std::cout << "剩余時間 : " << n << std::endl;
}
int main() {std::cout << "我的進程pid是: " << getpid() << std::endl;alarm(1); // 一次性的鬧鐘,超時alarm會自動被取消signal(SIGALRM, handler);while (true) {pause(); // 等待信號std::cout << "我醒來了..." << std::endl;}return 0;
}
編譯運行后,每隔 1 秒會輸出計數和剩余時間,實現了重復鬧鐘的功能。pause
函數會使調用進程(或線程)睡眠,直到收到一個終止進程的信號或一個導致調用信號捕捉函數的信號。
2.5 硬件異常產生信號
硬件異常被硬件檢測到并通知內核,然后內核向當前進程發送適當的信號。例如:
- 除零異常:當進程執行除以 0 的指令時,CPU 的運算單元會產生異常,內核將這個異常解釋為
SIGFPE
(8 號信號)發送給進程。 - 示例
#include <stdio.h> #include <signal.h> void handler(int sig) {printf("catch a sig : %d\n", sig); } int main() {signal(SIGFPE, handler); // 8) SIGFPEsleep(1);int a = 10;a /= 0;while (1);return 0; }
編譯運行后,會不斷輸出 “catch a sig : 8”,因為除零異常一直存在,內核會持續發送SIGFPE
信號。
- 非法內存訪問:當進程訪問了非法內存地址,MMU(內存管理單元)會產生異常,內核將這個異常解釋為
SIGSEGV
(11 號信號)發送給進程。- 示例
#include <stdio.h>
#include <signal.h>
void handler(int sig) {printf("catch a sig : %d\n", sig);
}
int main() {signal(SIGSEGV, handler);sleep(1);int *p = NULL;*p = 100;while (1);return 0;
}
編譯運行后,會不斷輸出 “catch a sig : 11”,因為非法內存訪問的異常一直存在。
三、信號遞達和阻塞的概念與原理
3.1 相關概念
- 信號遞達(Delivery):實際執行信號的處理動作。?
- 信號未決(Pending):信號從產生到遞達之間的狀態。
- 信號阻塞(Block):進程可以選擇阻塞某個信號,被阻塞的信號產生時將保持在未決狀態,直到進程解除對此信號的阻塞,才執行遞達的動作。
注意:阻塞和忽略是不同的,只要信號被阻塞就不會遞達,而忽略是在遞達之后可選的一種處理動作。
3.2 在內核中的表示
在進程控制塊(task_struct
)中,有三個與信號相關的重要部分:
block
:信號屏蔽字,用sigset_t
類型表示,每個 bit 代表對應信號是否被阻塞。pending
:未決信號集,同樣用sigset_t
類型表示,每個 bit 代表對應信號是否處于未決狀態。handler
:信號處理動作數組,每個元素是一個函數指針,指向該信號的處理函數,若為SIG_DFL
表示默認處理動作,SIG_IGN
表示忽略該信號。
例如,對于SIGINT
(2 號信號),若block
中對應的 bit 為 1,表示該信號被阻塞;若pending
中對應的 bit 為 1,表示該信號處于未決狀態;handler
中對應的函數指針指向該信號的處理函數。
3.3 信號集操作函數
sigset_t
類型用于表示信號集,我們不能直接操作sigset_t
變量的內部數據,需要使用專門的函數:
3.3.1 初始化和修改信號集
sigemptyset
:初始化信號集,使其中所有信號的對應 bit 清零,表示該信號集不包含任何有效信號。
#include <signal.h>
int sigemptyset(sigset_t *set);
sigfillset
:初始化信號集,使其中所有信號的對應 bit 置位,表示該信號集包含系統支持的所有信號。
#include <signal.h>
int sigfillset(sigset_t *set);
sigaddset
:在信號集中添加某種有效信號。
#include <signal.h>
int sigaddset(sigset_t *set, int signo);
sigdelset
:在信號集中刪除某種有效信號。
#include <signal.h>
int sigdelset(sigset_t *set, int signo);
- 返回值:以上四個函數成功返回 0,出錯返回 - 1。
3.3.2 判斷信號是否在信號集中
sigismember
:判斷一個信號集的有效信號中是否包含某種信號,若包含則返回 1,不包含則返回 0,出錯返回 - 1。
#include <signal.h>
int sigismember(const sigset_t *set, int signo);
3.3.3 讀取或更改進程的信號屏蔽字
sigprocmask
:讀取或更改進程的信號屏蔽字(阻塞信號集)。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
- 參數說明:
how
:指示如何更改信號屏蔽字,取值如下:
SIG_BLOCK
:set
包含了我們希望添加到當前信號屏蔽字的信號,相當于mask = mask | set
。SIG_UNBLOCK
:set
包含了我們希望從當前信號屏蔽字中解除阻塞的信號,相當于mask = mask & ~set
。SIG_SETMASK
:設置當前信號屏蔽字為set
所指向的值,相當于mask = set
。set
:若非空指針,則根據how
更改進程的信號屏蔽字;若為空指針,則不更改信號屏蔽字。oset
:若非空指針,則讀取進程的當前信號屏蔽字通過oset
參數傳出;若為空指針,則不讀取信號