上篇文章我們介紹了Syetem V IPC的消息隊列和信號量,那么信號量和我們下面要介紹的信號有什么關系嗎?其實沒有關系,就相當于我們日常生活中常說的老婆和老婆餅,二者并沒有關系
1. 認識信號
1.1 生活角度的信號解釋(快遞比喻)
- 識別信號(快遞到來)?:正如你等待快遞時能識別快遞員的到來,進程也能識別信號的產生。這是因為信號識別是操作系統內核的內置特性,由內核程序員實現。進程通過內核維護的數據結構(如位圖表)自動檢測信號是否產生,無需用戶干預。這類似于你知道快遞到來時會收到通知,但識別機制是預先定義的。
- 處理方法的預先準備:在信號產生之前,進程已經知道如何處理信號。例如,你知道快遞到來時該如何處理(打開、贈送或忽略),進程也預先定義了信號的處理動作(默認、自定義或忽略)。這通過內核中的“handler表”(一個函數指針數組)實現,其中存儲了每個信號的處理函數。如果信號未產生,進程仍然“知道”處理方法,因為handler表在進程啟動時已初始化。
- 非立即處理(延遲執行)?:信號產生后不一定會立即處理,就像快遞到來時你可能在打游戲而延遲5分鐘取件。進程可能因執行更高優先級的任務(如系統調用)而阻塞信號,導致信號處于“未決”(Pending)狀態,直到合適時機(如進程從內核態切換到用戶態)才處理。這體現了信號處理的異步性和優先級機制。
- 時間窗口和信號保存:從信號產生到處理之間有一個時間窗口,信號被保存在“未決”狀態。這類似于你知道快遞已到樓下但還未取件的階段,進程通過pending位圖記錄信號是否已產生但未處理(位圖比特位置1表示未決)。信號保存確保進程在后續能“記住”信號,避免丟失。
- 處理方式(遞達后的動作)?:當信號被處理(遞達)時,進程執行三種動作之一,對應快遞處理方式:
- 默認動作:如幸福地打開快遞(使用商品)。在信號處理中,默認動作通常是終止進程(如SIGINT)或忽略(如SIGUSR1),具體由內核定義。
- 自定義動作:如將零食送給女朋友。進程可注冊用戶自定義函數(通過
sigaction
),在信號遞達時執行特定邏輯(如打印日志或修改數據)。 - 忽略動作:如取件后扔掉快遞繼續打游戲。進程可明確忽略信號(使用SIG_IGN),但某些信號(如SIGKILL)不能被忽略。
- 異步特性:快遞到來時間不可預測,類似信號產生是異步的——它可能由外部事件(如用戶輸入Ctrl+C)、內核或其他進程觸發,進程無法準確預知信號何時產生。這要求信號機制必須支持非阻塞保存和延遲處理。
基本結論
- 識別信號的內置特性:進程識別信號依賴于內核維護的數據結構,如pending位圖(記錄信號是否未決)和block位圖(記錄是否阻塞)。識別是自動的,由內核實現,無需用戶代碼干預。例如,描述進程通過“兩個位圖 + 一個函數指針數組”識別信號,這在內核中是硬編碼的。
- 處理方法的預先準備:信號處理動作在信號產生前已定義,存儲在handler表中。程序可調用
sigaction
設置處理方式(默認SIG_DFL、忽略SIG_IGN或自定義函數)。如果未設置,內核使用默認動作。這確保了即使信號未產生,進程也知道如何處理。 - 非立即處理(合適時機處理)?:信號不立即處理的原因包括阻塞(Block)和優先級。阻塞時信號保持在未決狀態,直到解除阻塞(如調用
sigprocmask
)。處理時機通常在進程從內核態返回用戶態時,確保系統穩定性。和用快遞延遲比喻解釋此機制。 - 信號保存和處理階段:信號生命周期分為三個階段:
- 產生(Generation)?:信號由事件觸發(如鍵盤中斷)。
- 保存(Pending)?:信號記錄在pending位圖中,處于未決狀態。
- 遞達(Delivery)?:信號被處理,執行handler表中定義的動作。
阻塞信號會延長未決狀態,直到阻塞解除。例如,和定義未決為“信號從產生到遞達之間的狀態”,并用位圖實現保存。
- 捕捉方式(信號處理動作)?:處理動作統稱為“信號捕捉”,包括:
- 默認(SIG_DFL)?:系統預定義行為,如終止進程。
- 忽略(SIG_IGN)?:丟棄信號,不執行任何操作。
- 自定義:用戶定義函數執行特定邏輯。
1.2 簡單樣例
我們先來一個簡單的樣例,來認識一下進程中的信號
#include <iostream>
#include <sys/types.h>
#include <unistd.h>int main()
{while (true){std::cout << "I am a process, pid: " << getpid() << std::endl;sleep(1);}return 0;
}
運行結果:
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_sig/Sig$ make
g++ -o testsig testsig.cc -std=c++11
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_sig/Sig$ ./testsig
I am a process, pid: 255198
I am a process, pid: 255198
I am a process, pid: 255198
I am a process, pid: 255198
I am a process, pid: 255198
I am a process, pid: 255198
^C #用戶按下Ctrl+C
用戶輸入命令啟動Shell前臺進程后:
- 當用戶按下Ctrl+C時,系統會捕獲這個鍵盤輸入產生的硬件中斷
- 操作系統將該中斷解釋為信號并發送給目標前臺進程
- 前臺進程收到信號后觸發終止流程,最終退出執行
實際上,Ctrl+C 的本質是向前臺進程發送 SIGINT(即 2 號信號)。為了驗證這一點,我們需要引入一個系統調用函數來進行演示。
signal系統調用
更多相關內容可以通過man手冊來查看
一、signal
?系統調用的核心功能與定義
signal()
?是 Linux 中用于修改進程對特定信號處理行為的系統調用,其核心功能包括:
- 捕獲信號:注冊自定義處理函數,替代默認行為(如?
Ctrl+C
?觸發?SIGINT
?時執行自定義邏輯)。 - 忽略信號:指定?
SIG_IGN
?使進程完全忽略信號(如?SIGCHLD
?避免僵尸進程)。 - 恢復默認:指定?
SIG_DFL
?還原內核默認行為(如?SIGTERM
?終止進程)。
函數原型與參數解析
#include <signal.h> // 復雜聲明(傳統寫法)
void (*signal(int signum, void (*handler)(int)))(int); // 簡化類型定義(POSIX 標準)
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signum
:信號編號(如?SIGINT=2
,?SIGKILL=9
)。handler
:處理函數指針,或預定義常量?SIG_IGN
/SIG_DFL
。
二、內核實現機制與關鍵行為
1. 處理函數注冊流程
內核通過進程的?
task_struct
?維護?struct sigaction
?數組,存儲每個信號的處理配置。調用?
signal()
?時,內核更新對應信號的?sa_handler
?字段:new_sa.sa.sa_handler = handler; new_sa.sa.sa_flags = SA_ONESHOT | SA_NOMASK; // 傳統 signal 的隱式標志 do_sigaction(signum, &new_sa, &old_sa); // 更新信號處理表
SA_ONESHOT
:處理函數執行一次后自動恢復為默認行為(遺留問題)。SA_NOMASK
:執行處理函數時不自動阻塞當前信號(可能導致重入)。
2. 信號遞達時的關鍵操作
當信號遞達(Delivery)時:
- 重置處理方式:若通過?
signal()
?注冊自定義函數,內核會先將處理方式重置為?SIG_DFL
(除非使用?sigaction
?顯式避免)。 - 執行處理函數:在用戶態調用注冊的?
handler(int sig)
。 - 阻塞機制:默認不阻塞同類型信號,可能導致處理函數被重入(需手動屏蔽)。
那我們下面就來捕獲一下,看看按下Ctrl+C后發送的是不是2號信號
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>void handlerSig(int sig)
{std::cout << "獲得了一個信號: " << sig << std::endl;
}int main()
{signal(SIGINT, handlerSig);while (true){std::cout << "I am a process, pid: " << getpid() << std::endl;sleep(1);}return 0;
}
運行結果:
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_sig/Sig$ make
g++ -o testsig testsig.cc -std=c++11
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_sig/Sig$ ./testsig
I am a process, pid: 257107
I am a process, pid: 257107
I am a process, pid: 257107
I am a process, pid: 257107
^C獲得了一個信號: 2
I am a process, pid: 257107
I am a process, pid: 257107
I am a process, pid: 257107
^C獲得了一個信號: 2
I am a process, pid: 257107
I am a process, pid: 257107
I am a process, pid: 257107
I am a process, pid: 257107
I am a process, pid: 257107
^\Quit (core dumped)
我們可以看到,按下?Ctrl+C?后打印消息而非終止進程。所以,Ctrl+C?觸發了?SIGINT
?信號,此時會執行我們寫的自定義邏輯。這個時候我們?Ctrl+C 終止不了這個進程,但是我們還可以使用?Ctrl+\ 來結束這個進程。
也許你可能還是會有疑問:
1. 為什么進程不退出?
因為我們修改了?SIGINT
?信號的默認處理方式。
信號的默認行為 (Default Action):每個信號都有一個默認行為。對于?
SIGINT
?(信號編號2),其默認行為是?Term
,即終止進程。自定義行為 (Custom Action):在我們的代碼中,通過?
signal(SIGINT, handlerSig);
?這行代碼,告訴操作系統:“當我的進程收到?SIGINT
?信號時,請不要執行默認的終止操作,請轉而執行我提供的?handlerSig
?函數”。因此,當我們按下?
Ctrl+C
,信號產生了,進程并沒有終止,而是去執行了?cout << "獲得了一個信號: 2" << endl;
。
2. 為什么還可以用?Ctrl+\
?來結束進程?
因為?Ctrl+\
?會發送另一個不同的信號:SIGQUIT
?(信號編號3)。
SIGQUIT
?的默認行為:它的默認行為是?Core
,即終止進程并生成一個核心轉儲文件 (core dump),用于調試。在輸出中看到的?Quit (core dumped)
?就印證了這一點。我們沒有修改它的處理方式:我們的代碼只捕獲了?
SIGINT
,沒有捕獲?SIGQUIT
。所以當?SIGQUIT
?信號到來時,進程依然執行其默認行為——終止自己。
結合“快遞”類比解釋?Ctrl-C
?的處理過程
讓我們將快遞類比和這個?Ctrl-C
?的例子一一對應起來:
步驟 | 快遞場景 (你) | 信號處理 (進程) | 對應代碼/現象 |
---|---|---|---|
1. 識別與準備 | 你知道快遞來了該怎么處理(拆開、送人、忽略)。 | 進程提前知道收到?SIGINT ?該怎么處理。 | signal(SIGINT, handlerSig); |
2. 信號產生 | 快遞員到了樓下,給你打電話(通知到來)。 | 用戶按下?Ctrl+C ,硬件產生中斷,內核識別到并給前臺進程發送?SIGINT ?信號。 | 你按下?Ctrl+C |
3. 信號保存 | 你正在打游戲,記住“有快遞要取”,但不立即處理。 | 進程可能正在執行?cout ?或?sleep 。內核將?SIGINT ?信號標記在該進程的未決信號集中,等待處理。進程此時并不會被立即打斷。 | 按下?Ctrl+C ?后,sleep ?或?cout ?語句可能還會執行完。 |
4. 信號處理 | 你一局游戲打完(到達一個合適的時機),下樓取快遞并按照預定方式處理(比如送給女朋友)。 | 進程從內核態返回用戶態時(這是一個合適的時機,例如?sleep ?函數被信號中斷返回、或者一個系統調用結束),會檢查是否有未決信號。發現有?SIGINT ,于是執行自定義處理函數。 | 打印出?獲得了一個信號: 2 |
5. 行為差異 | 你只處理了A快遞(送人),但B快遞(水電費賬單)來了你還是會按默認方式處理(拆開查看)。 | 進程只自定義了?SIGINT ,SIGQUIT ?仍按默認方式處理(終止進程)。 | Ctrl+\ ?可以殺掉進程 |
解釋和補充
“signal函數僅僅是設置了特定信號的捕捉行為處理方式,并不是直接調用處理動作。”
signal()
?只是一個注冊或設置操作。它像是在門口貼了一張紙條:“如果快遞是零食(SIGINT
),請放在門口;如果是賬單(SIGQUIT
),照常敲門”。紙條本身不會引來快遞,只有快遞真正到來時,紙條上的指示才會被讀取和執行。
“Ctrl-C 產生的信號只能發給前臺進程。” && “Shell可以同時運行一個前臺進程和任意多個后臺進程”
這是Shell的工作機制。
&
?會將進程放到后臺運行,Shell會給它分配一個作業號(job number),但它無法接收來自終端的控制信號(如?Ctrl-C
,?Ctrl-\
,?Ctrl-Z
)。只有前臺進程獨占終端輸入,才能接收這些信號。
“信號相對于進程的控制流程來說是異步(Asynchronous)的”
這是信號最核心的特性。進程完全無法預測信號到來的準確時間。你的?
main
?函數中的代碼可能執行到?cout
、sleep
?或者任何一條指令時,信號都可能突然到來。進程的控制流程就像是在一條主路上開車,信號就像路邊突然出現的廣告牌或指示牌,你不知道它何時會出現,但出現時你就需要根據上面的信息做出反應(處理信號)。
總結一下:信號機制就是這樣一個?“異步通知”?機制,進程需要?“提前預約”?處理方式,然后在?“合適的時機”?去處理已經?“被記錄在案”?的信號。
前臺進程VS后臺進程
我們可以先來看一個現象
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_sig/Sig$ ./testsig
I am a process, pid: 262958
I am a process, pid: 262958
I am a process, pid: 262958
I am a process, pid: 262958
ls
I am a process, pid: 262958
I am a process, pid: 262958
I am a process, pid: 262958
pwI am a process, pid: 262958
d
I am a process, pid: 262958
I am a process, pid: 262958
I am a process, pid: 262958
I am a process, pid: 262958
I am a process, pid: 262958
I am a process, pid: 262958
I am a process, pid: 262958
I am a process, pid: 262958
I am a process, pid: 262958
I am a process, pid: 262958
^C獲得了一個信號: 2
I am a process, pid: 262958
I am a process, pid: 262958
I am a process, pid: 262958
I am a process, pid: 262958
我們可執行程序跑起來之后(此時是前臺進程),我們再輸入ls,pwd等指令是沒有用的,
那我們再來把該進程變為后臺進程,如:?./testsig &,也就是在后面加上&,執行這個命令后,我們的進程就成為了后臺進程
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_sig/Sig$ ./testsig &
[1] 263501
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_sig/Sig$ I am a process, pid: 263501
I am a process, pid: 263501
I am a process, pid: 263501
ls
Makefile testsig testsig.cc
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_sig/Sig$ I am a process, pid: 263501
I am a process, pid: 263501
I am a process, pid: 263501
pwdI am a process, pid: 263501/home/ltx/gitLinux/Linux_system/lesson_sig/Sig
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_sig/Sig$ I am a process, pid: 263501
I am a process, pid: 263501
I am a process, pid: 263501
I am a process, pid: 263501
I am a process, pid: 263501
I am a process, pid: 263501
I am a process, pid: 263501
I am a process, pid: 263501
I am a process, pid: 263501
I am a process, pid: 263501
I am a process, pid: 263501
變成后臺進程后,我們可以看到,使用ls, pwd等命令就沒有問題了
注意:Ctrl+C只能終止前臺進程,并不能終止后臺進程
這是為什么呢?如何查看后臺進程呢?那后臺進程又如何終止呢?
為什么前臺進程會“霸占”終端,而后臺進程不會?
這完全是由?Shell?的行為決定的,目的是為了提供良好的人機交互體驗。
前臺進程 (Foreground Process)
當你直接執行?
./testsig
?時,Shell 會將自己掛起,并將終端的標準輸入(stdin)、標準輸出(stdout)、標準錯誤(stderr)的控制權完全交給這個子進程。此時,
testsig
?進程是終端的“前臺所有者”。你輸入的每一個字符(l
,?s
,?p
,?w
,?d
,?Enter
)都會直接發送給它,而不是 Shell。你的?
testsig
?程序在設計上只會在循環中打印信息和睡眠,它并沒有編寫處理?ls
、pwd
?這些命令的邏輯,所以這些你輸入的字符對它來說是無意義的,它不會做出你期望的響應。你看到的?ls
?和?pwI am a process...
?混雜的輸出,正是因為?testsig
?的打印和你的輸入同時競爭同一個終端輸出造成的混亂。
后臺進程 (Background Process)
當你使用?
&
?執行?./testsig &
?時,Shell 會創建一個子進程來運行程序,但不會將自己掛起,也不會將終端的標準輸入交給它。終端的標準輸入(你的鍵盤輸入)始終由 Shell 自己管理。因此,你之后輸入的?
ls
,?pwd
?等命令都會被 Shell 正常接收并解釋執行。后臺進程?
testsig
?仍然擁有向終端輸出的權利(所以你能看到它的打印信息),但它無法從終端讀取輸入。如果它嘗試讀取輸入,Shell 會將其自動掛起(Stopped),以防止它阻塞等待一個永遠無法獲得的輸入。
為什么?Ctrl+C
?只能終止前臺進程?
Ctrl+C
?產生的?SIGINT
?信號,內核會發送給當前擁有該終端的前臺進程組中的所有進程。當你運行前臺?
./testsig
?時,它的進程組就是前臺進程組,所以它能收到?SIGINT
。當你運行后臺?
./testsig &
?時,它的進程組不再是前臺進程組。你此時在終端輸入的?Ctrl+C
,內核會將其發送給當前的前臺進程組,也就是?Shell 本身。Shell 收到這個信號后,通常不會終止自己,而是會忽略它或者用它來做一些其他的交互提示(比如給你一個新提示符),所以后臺進程完全收不到這個信號。
如何管理后臺進程?
1. 查看后臺進程
使用 Shell 內置命令?jobs
。
$ ./testsig &
[1] 263501 # [job_number] pid
$ jobs
[1]+ Running ./testsig &
[1]
:作業編號(Job Number),由 Shell 分配管理,在當前 Shell 會話內有效。263501
:進程ID(PID),由操作系統分配,在整個系統內有效。
2. 將后臺進程拉回前臺
使用?fg %<job_number>
。這樣你就能再用?Ctrl+C
?來終止它了。
$ fg %1 # 將作業編號為1的后臺作業變為前臺作業
# 此時終端再次被 testsig 霸占,可以用 Ctrl+C 終止
3. 終止后臺進程
既然?Ctrl+C
?無效,就必須使用?kill
?命令通過發送信號來終止。
方法一:通過作業編號(推薦,在當前終端內操作最方便)
$ kill %1 # 向作業1發送默認的 TERM 信號,請求終止
$ jobs
[1]+ Terminated ./testsig # 確認它已終止
方法二:通過進程ID(PID)
$ kill 263501 # 發送 SIGTERM (15),友好地請求終止
$ kill -9 263501 # 發送 SIGKILL (9),強制殺死,無法被捕獲或忽略
示例:
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_sig/Sig$ ./testsig
I am a process, pid: 265199
I am a process, pid: 265199
I am a process, pid: 265199
I am a process, pid: 265199
I am a process, pid: 265199
^Z
[1]+ Stopped ./testsig
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_sig/Sig$ jobs
[1]+ Stopped ./testsig
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_sig/Sig$ fg %1
./testsig
I am a process, pid: 265199
I am a process, pid: 265199
I am a process, pid: 265199
I am a process, pid: 265199
I am a process, pid: 265199
I am a process, pid: 265199
I am a process, pid: 265199
^C獲得了一個信號: 2
I am a process, pid: 265199
I am a process, pid: 265199
I am a process, pid: 265199
I am a process, pid: 265199
^\Quit (core dumped)
注意:Ctrl+Z 可以將前臺進程暫停,此時會成為后臺進程
你可能又會問,為什么剛剛使用&運行后臺進程的時候還會一直在終端上打印,那我Ctrl+Z就不行呢?
核心區別:進程狀態
當使用不同方式創建"后臺進程"時,進程的實際狀態是不同的:
&
?啟動的后臺進程:處于?運行中 (Running)?狀態。它一直在執行,只是沒有控制終端輸入。Ctrl+Z
?暫停的進程:被置于?停止 (Stopped)?狀態。它被暫停執行了,就像被按下了"暫停鍵"。
我們可以分別在這兩種情況時通過ps指令查看
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_sig/Sig$ ps ajx | grep testsig254117 265199 265199 254117 pts/6 254117 T 1004 0:00 ./testsig263627 265245 265244 263627 pts/1 265244 S+ 1004 0:00 grep --color=auto testsig
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_sig/Sig$ ps ajx | grep testsig254117 265600 265600 254117 pts/6 254117 S 1004 0:00 ./testsig263627 265603 265602 263627 pts/1 265602 S+ 1004 0:00 grep --color=auto testsig
注意:+號代表前臺進程
那有沒有辦法將暫停的后臺進程,重新在后臺運行起來呢?可以使用bg命令(和fg用法相同),如下:
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_sig/Sig$ ./testsig
I am a process, pid: 265703
I am a process, pid: 265703
I am a process, pid: 265703
I am a process, pid: 265703
I am a process, pid: 265703
^Z
[1]+ Stopped ./testsig
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_sig/Sig$ bg %1
[1]+ ./testsig &
I am a process, pid: 265703
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_sig/Sig$ I am a process, pid: 265703
I am a process, pid: 265703
I am a process, pid: 265703
I am a process, pid: 265703
I am a process, pid: 265703
...
詳細解釋
1. 使用?&
?啟動后臺進程
$ ./testsig &
[1] 263501
$ I am a process, pid: 263501
進程狀態:
Running
?(運行中)終端訪問:保留了向終端輸出的權限(stdout/stderr)
機制:
Shell 創建子進程后,立即繼續運行,不等待子進程結束。
子進程被放入后臺進程組,但仍然可以自由地向終端輸出。
這就是為什么你能看到?
I am a process...
?與 Shell 提示符和命令輸出交錯顯示的原因 - 兩個進程在競爭同一個輸出設備。
2. 使用?Ctrl+Z
?然后?bg
$ ./testsig
^Z # 按下 Ctrl+Z
[1]+ Stopped ./testsig
$ bg %1 # 將其在后臺繼續運行
[1]+ ./testsig &
# 此時不再有輸出
Ctrl+Z
?的效果:向進程發送?SIGTSTP
?(Terminal Stop) 信號進程狀態變化:從?
Running
?變為?Stopped
?(停止)bg
?命令的效果:向進程發送?SIGCONT
?(Continue) 信號,但不將其帶回前臺關鍵點:雖然?
bg
?讓進程繼續執行,但 Shell 和內核對其處理方式與直接用?&
?啟動的進程有細微差別
總結與類比
特性 | 前臺進程 | 后臺進程 (& ) |
---|---|---|
終端輸入 (stdin) | 獨占 | 無法獲取(讀取會被掛起) |
終端輸出 (stdout/stderr) | 獨占 | 共享(輸出會與Shell提示符等混雜) |
控制信號 (Ctrl+C ) | 可以接收 | 無法接收(信號發給Shell) |
Shell 狀態 | Shell 被掛起,等待其結束 | Shell 繼續運行,可接受新命令 |
管理命令 | Ctrl+C ,?Ctrl+Z ,?Ctrl+\ | jobs ,?kill %n ,?fg %n ,?bg %n |
一個簡單的比喻:
前臺進程?就像你正在全屏玩的一款游戲,鍵盤和顯示器都被它獨占,你無法同時做別的事。
后臺進程?就像你在電腦上開啟了一個音樂播放器然后最小化,音樂在放(輸出),但你可以在前臺用瀏覽器(Shell)做其他事情,并且你不能直接用鍵盤控制播放器(除非你把它切回前臺)。想關掉音樂播放器,你不能在瀏覽器里按“關機鍵”,必須去任務管理器(
kill
)里結束它。
1.3 信號概念
信號是進程間通信(IPC)的一種重要機制,它提供了一種異步事件通知的方式。在Unix/Linux系統中,信號本質上是一種軟件中斷,用于通知進程發生了某個特定事件或異常情況。
信號的主要特點包括:
- 異步性:信號可以在任何時候發送給進程,進程無法預知信號何時到達
- 軟中斷:信號機制在軟件層面模擬了硬件中斷的行為
- 基本通信:信號提供了最基本的進程間通信方式
常見的信號類型有:
- SIGINT(2):中斷信號,通常由Ctrl+C觸發
- SIGKILL(9):強制終止進程信號
- SIGSEGV(11):段錯誤信號
- SIGTERM(15):終止信號
信號處理流程:
- 信號產生:由內核、其他進程或終端產生
- 信號傳遞:內核將信號傳遞給目標進程
- 信號處理:目標進程執行預先注冊的信號處理函數
在實際應用中,信號常用于:
- 進程間簡單通信
- 系統異常處理
- 進程控制(如終止、暫停等)
- 用戶交互響應
查看信號
我們可以使用 kill -l 命令來查看有哪些信號
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_sig/Sig$ kill -l1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
注意:1~31是普通信號,34~64是實時信號(實時信號在后文中不做考慮)
每個信號都對應一個編號和宏定義名稱,這些宏定義可在signal.h頭文件中查詢。例如,該文件中定義了
還可以通過man手冊查詢,如:man 7 signal
普通信號的特點是:不支持排隊。如果同一個信號在進程處理它之前多次產生,它只會被記錄一次,這可能會導致信號丟失。
普通標準信號詳解表
下表列出了最常見和重要的標準信號:
編號 | 信號名稱 | 默認行為 | 觸發場景說明 | 可否捕獲或忽略 | 重要說明 |
---|---|---|---|---|---|
1 | SIGHUP | Term | 掛起。終端連接斷開(如網絡斷開、關閉終端窗口)、控制進程終止。 | Yes | 常被用于通知守護進程重新讀取配置文件(如?nginx -s reload )。 |
2 | SIGINT | Term | 中斷。來自鍵盤的中斷,通常是用戶按下?Ctrl+C 。 | Yes | 請求優雅地終止前臺進程。 |
3 | SIGQUIT | Core | 退出。來自鍵盤的退出,通常是用戶按下?Ctrl+\ 。 | Yes | 不僅終止進程,還會生成?core dump?文件用于調試。表示用戶希望進程終止并留下調試信息。 |
4 | SIGILL | Core | 非法指令。進程嘗試執行一條非法、錯誤或特權的指令。 | Yes | 通常由程序 bug?引起,例如執行了損壞的二進制文件、棧溢出等。 |
5 | SIGTRAP | Core | 跟蹤/斷點陷阱。由調試器使用,用于在斷點處中斷進程的執行。 | Yes | 這是調試器(如 gdb)實現斷點功能的機制。 |
6 | SIGABRT | Core | 中止。通常由?abort() ?函數調用產生。 | Yes | 進程自己調用?abort() ?來終止自己,通常表示檢測到了嚴重的內部錯誤(如?assert ?斷言失敗)。也會生成 core dump。 |
7 | SIGBUS | Core | 總線錯誤。無效的內存訪問,即訪問的內存地址不存在或違反了內存對齊要求。 | Yes | 硬件級別的錯誤。例如,在支持對齊要求的架構上訪問未對齊的地址。與?SIGSEGV ?類似但原因更底層。 |
8 | SIGFPE | Core | 浮點異常。錯誤的算術運算,如除以零、溢出等。 | Yes | 不僅是浮點數,整數除以零也會觸發此信號。 |
9 | SIGKILL | Term | 殺死。無條件立即終止進程。 | No | 無法被捕獲、阻塞或忽略。是終止進程的最終極、最強制的手段。kill -9 ?的由來。 |
10 | SIGUSR1 | Term | 用戶自定義信號 1。 | Yes | 沒有預定義的含義,完全留給用戶程序自定義其行為。常用于應用程序內部通信(如通知進程切換日志文件、重新加載特定數據等)。 |
11 | SIGSEGV | Core | 段錯誤。無效的內存引用,即訪問了未分配或沒有權限訪問的內存(如向只讀內存寫入)。 | Yes | C/C++ 程序中最常見的崩潰原因之一(解空指針、訪問已釋放內存、棧溢出、緩沖區溢出等)。 |
12 | SIGUSR2 | Term | 用戶自定義信號 2。 | Yes | 同上,另一個用戶可自定義用途的信號。 |
13 | SIGPIPE | Term | 管道破裂。向一個沒有讀者的管道(或 socket)進行寫入操作。 | Yes | 常見場景:一個管道中,讀端進程已關閉或終止,寫端進程還在寫入。如果忽略此信號,write ?操作會返回錯誤并設置?errno ?為?EPIPE 。 |
14 | SIGALRM | Term | 定時器信號。由?alarm() ?或?setitimer() ?設置的定時器超時后產生。 | Yes | 常用于實現超時機制或周期性任務。 |
15 | SIGTERM | Term | 終止。這是一個友好的終止進程的請求。 | Yes | kill ?命令的默認信號。進程收到此信號后,應該執行清理工作(關閉文件、釋放資源等)然后退出。是優雅關閉服務的首選方式。 |
16 | SIGSTKFLT | Term | 協處理器棧錯誤。極少使用。 | Yes | 與早期的數學協處理器有關,現代 Linux 系統上基本不會見到。 |
17 | SIGCHLD | Ign | 子進程狀態改變。一個子進程停止或終止時,內核會向父進程發送此信號。 | Yes | 非常重要!父進程可以通過捕獲此信號來調用?wait() ?或?waitpid() ?回收子進程資源,防止出現僵尸進程。默認行為是 Ignore,但最好顯式處理。 |
18 | SIGCONT | Cont | 繼續。讓一個停止的進程繼續運行。 | Yes | 無法被停止的進程忽略。常用于作業控制(fg ?/?bg ?命令的底層實現)。 |
19 | SIGSTOP | Stop | 停止。暫停進程的執行(進入 Stopped 狀態)。 | No | 無法被捕獲、阻塞或忽略。是?Ctrl+Z ?(SIGTSTP ) 的強制版本。 |
20 | SIGTSTP | Stop | 終端停止。來自終端的停止信號,通常是用戶按下?Ctrl+Z 。 | Yes | 請求優雅地暫停前臺進程。進程可以被捕獲,在捕獲函數中它可以做一些準備工作后再決定是否暫停自己。 |
21 | SIGTTIN | Stop | 后臺進程讀終端。一個后臺進程嘗試從控制終端讀取輸入。 | Yes | 為了防止后臺進程干擾前臺,內核會自動停止該后臺進程。 |
22 | SIGTTOU | Stop | 后臺進程寫終端。一個后臺進程嘗試向控制終端寫入輸出。 | Yes | 類似于?SIGTTIN ,但用于寫入操作。是否停止取決于終端配置(stty tostop )。 |
... | ... | ... | ... | ... | ... |
說明:
默認行為:
Term
: 終止進程
Core
: 終止進程并生成 core dump 文件
Ign
: 忽略信號
Stop
: 暫停(停止)進程
Cont
: 如果進程已停止,則繼續運行
SIGKILL
?(9) 和?SIGSTOP
?(19) 是兩個特殊的信號,無法被捕獲、阻塞或忽略。這是為了給系統管理員一個最終能控制任何進程的手段。
核心總結與要點
信號來源:
硬件異常:由 CPU 檢測到錯誤產生(如?
SIGSEGV
,?SIGFPE
,?SIGILL
)。終端交互:用戶通過鍵盤產生(如?
SIGINT
,?SIGQUIT
,?SIGTSTP
)。軟件事件:由軟件條件觸發(如?
SIGPIPE
,?SIGCHLD
,?SIGALRM
)。系統調用/命令:由?
kill()
?函數或?kill
?命令發出。
處理方式:
執行默認操作:大多數信號的默認操作是終止進程。
忽略信號:
SIG_IGN
,但?SIGKILL
?和?SIGSTOP
?不能忽略。自定義信號處理函數:
signal()
?或更強大的?sigaction()
。
不可靠信號與可靠信號:
普通信號 (1-31)?是不可靠信號,因為它們不支持排隊,可能會丟失。
實時信號 (34-64,?
SIGRTMIN
?到?SIGRTMAX
)?是可靠信號,支持排隊,多個相同的信號會被依次處理,不會丟失。它們沒有預定義含義,完全由應用程序使用。
最佳實踐:
使用?
SIGTERM
?(15) 來優雅地終止進程,給進程一個清理現場的機會。僅在進程不響應?
SIGTERM
?時,使用?SIGKILL
?(9) 作為最后手段。在編寫服務器或長時間運行的程序時,妥善處理?
SIGHUP
(重讀配置)和?SIGUSR1
/SIGUSR2
(自定義行為)。父進程一定要處理?
SIGCHLD
?信號,以避免產生僵尸進程。
所以信號的核心特性我們可以來做一下總結
核心特性
異步通信機制
- 信號是最短小的進程間消息(僅攜帶信號編號),用于通知進程特定事件發生(如用戶中斷、內存錯誤等)。
- 本質是內核向用戶態進程推送的軟中斷,進程可能在任何代碼位置被信號中斷。
不可靠性與局限性
- 無排隊機制:連續發送相同信號時,進程可能僅收到一次(實時信號支持排隊)。
- 信息量極小:僅傳遞信號編號,無法攜帶附加數據(實時信號可攜帶)。
- 部分信號不可控:
SIGKILL
(9)和SIGSTOP
(19)無法被阻塞、捕獲或忽略。
生命周期三階段
- ???????未決(Pending)?:信號產生后暫存于內核位圖,等待遞達。
- 阻塞(Block)?:進程可主動屏蔽信號,延遲處理。
2. 產生信號
至此,我們對信號有了一個基本認識,那么接下來我們就先從信號的產生介紹
在前文中,我們知道通過鍵盤就能產生信號(Ctrl+C,Ctrl+\和Ctrl+Z)
下面我們就來介紹其他幾種產生信號的方式
2.1 系統調用命令
在前文中我們知道,后臺進程對于鍵盤輸入(Ctrl+C)產生的信號,不能接收也不做反應,所以當時我們提到了可以使用kill命令來發送信號,從而終止后臺進程。在系統調用中同樣也有kill函數,也可以發送信號。
1.?kill
?- 向指定進程或進程組發送信號
kill()
?是最核心、最通用的信號發送函數,它的名字有點誤導性,因為它不僅可以發送終止信號(如?SIGKILL
),還可以發送任何信號。
函數原型
#include <sys/types.h>
#include <signal.h>int kill(pid_t pid, int sig);
參數?
pid
:指定目標進程或進程組,取值及其含義非常關鍵:pid
?值含義 > 0
將信號發送給進程ID為? pid
?的特定進程。0
將信號發送給與調用進程屬于同一個進程組的所有進程(包括自己)。 -1
將信號發送給調用進程有權限發送信號的所有進程(除了 init 進程和自身)。 規則復雜,較少使用。 < -1
將信號發送給進程組ID為? -pid
?的所有進程。例如?pid = -1234
?發送給 PGID 為 1234 的進程組。參數?
sig
:要發送的信號編號(如?SIGINT
,?SIGTERM
)。如果?sig
?為?0
,則不發送任何信號,但依然會執行錯誤檢查(用于檢查目標進程是否存在)。返回值:成功返回?
0
;失敗返回?-1
?并設置?errno
。
關鍵特性與工作原理
權限檢查:
kill()
?調用會進行嚴格的權限檢查。超級用戶(root)?可以向任何進程發送信號,而普通用戶只能向屬于自己的進程發送信號。否則調用會失敗,errno
?被設置為?EPERM
。NULL 信號 (
sig=0
):這是一個非常有用的特性。它用于檢測目標進程是否存在且是否有權限向其發送信號。如果?kill(pid, 0)
?成功返回,說明進程存在且有權限;如果返回?-1
?且?errno
?為?ESRCH
,則進程不存在;如果為?EPERM
,則無權限。底層機制:當?
kill()
?被調用時,內核會檢查參數有效性及權限。如果通過,內核就會在目標進程(或進程組中每個進程)的?task_struct
?中的未決信號集(pending)?里設置對應的信號位。至于目標進程何時以及如何處理這個信號,就取決于它的信號掩碼和處理函數了,這與?kill()
?調用本身異步。
示例:
這里我們使用kill系統調用來實現一個我們自己的kill命令
#include <iostream>
#include <sys/types.h>
#include <signal.h>
#include <string>// ./mykill signalnum pid
int main(int argc, char* argv[])
{if(argc != 3){std::cout << "./mykill signalnum pid" << std::endl;return 1;}int signalnum = std::stoi(argv[1]);pid_t target = std::stoi(argv[2]); int n = kill(target, signalnum);if(n == 0){std::cout << "send " << signalnum << " to " << target << " success" << std::endl;}return 0;
}
運行結果:
我們分別使用內置命令kill和我們自己實現的mykill給 testsig進程 發送2號信號,都可以被捕獲
2.?raise
?- 向當前進程發送信號
raise()
?是一個簡化版的?kill()
,它的目標只有一個,就是調用者進程自身。
函數原型
#include <signal.h>int raise(int sig);
參數?
sig
:要發送給自己的信號編號。返回值:成功返回?
0
;失敗返回非零值。
關鍵特性與工作原理
單線程程序:在單線程程序中,
raise(sig)
?幾乎等價于?kill(getpid(), sig)
。多線程程序:在多線程環境中,
raise(sig)
?的含義是將信號發送給調用它的特定線程,而不是整個進程。這是它與?kill(getpid(), sig)
?的一個重要區別,后者會將信號發送給進程中的任意一個線程。便捷性:它的存在純粹是為了方便,讓代碼意圖更清晰——“我要給自己發個信號”。
示例:
這里我們把普通信號都捕獲了,方便我們使用raise系統調用給當前進程發送信號時,都能捕獲到然后打印出來我們查看
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>void handlerSig(int sig)
{std::cout << "獲得了一個信號: " << sig << std::endl;
}int main()
{for(int i = 1; i < 32; i++)signal(i, handlerSig);for(int i = 1; i < 32; i++){sleep(1);raise(i);}while (true){std::cout << "I am a process, pid: " << getpid() << std::endl;sleep(1);}return 0;
}
運行結果:
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_sig/Sig$ ./testsig
獲得了一個信號: 1
獲得了一個信號: 2
獲得了一個信號: 3
獲得了一個信號: 4
獲得了一個信號: 5
獲得了一個信號: 6
獲得了一個信號: 7
獲得了一個信號: 8
Killed
在前文信號概念中,我們有說過,有兩個信號9號和19號信號不能被捕獲,所以當我們進程在準備捕獲9號信號時,由于9號信號不能被捕獲,所以當前進程被終止
3.?abort
?- 使當前進程異常終止
abort()
?函數的功能非常明確和強硬:立即異常終止當前進程,并生成一個 core dump 文件(如果系統配置允許)。
函數原型
#include <stdlib.h>void abort(void);
// 注意:該函數無參數,且無返回值,因為它永遠不會返回到調用者。
關鍵特性與工作原理
不可阻擋:
abort()
?函數會無條件地終止進程。它會首先解除對?SIGABRT
?信號的阻塞,然后向自己發送?SIGABRT
?信號。信號處理:
如果進程為?
SIGABRT
?設置了自定義處理函數,abort()
?會先調用這個函數。關鍵點:如果自定義處理函數沒有終止進程(例如,它調用了?
longjmp
?跳轉走了),那么?abort()
?函數在自定義處理函數返回后,會確保進程被強制終止(通常是恢復?SIGABRT
?的默認行為并再次發送它)。這意味著你無法真正“捕獲”?abort()
?來阻止進程終止。
刷新緩沖區:
abort()
?會刷新并關閉所有標準 I/O 流(類似于?fflush(NULL)
),但這不保證所有數據都能正確寫入(因為終止是強制的)。生成 Core Dump:其默認行為是產生?
SIGABRT
?信號,該信號的默認動作是?Core
,所以它會創建一個 core dump 文件,用于事后調試,幫助定位程序調用?abort()
?的位置和原因。
示例:
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>void handlerSig(int sig)
{std::cout << "獲得了一個信號: " << sig << std::endl;
}int main()
{for(int i = 1; i < 32; i++)signal(i, handlerSig);while (true){std::cout << "I am a process, pid: " << getpid() << std::endl;sleep(1);abort();}return 0;
}
運行結果:
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_sig/Sig$ ./testsig
I am a process, pid: 22993
獲得了一個信號: 6
Aborted (core dumped)
可以看到我們捕獲了6號信號,也就是SIGABRT
?信號,同時進程也被終止了
總結與對比
特性 | kill() | raise() | abort() |
---|---|---|---|
目標 | 任意進程或進程組 | 僅調用者自身(或當前線程) | 強制終止調用者自身 |
主要用途 | 進程間通信(IPC)、管理系統進程 | 進程內部觸發信號處理邏輯 | 緊急情況下的程序終止(斷言失敗、嚴重錯誤) |
靈活性 | 極高,可發送任何信號 | 高,可發送任何信號給自己 | 無,固定產生?SIGABRT |
返回值 | int ?(0成功, -1失敗) | int ?(0成功, 非0失敗) | void ?(永不返回) |
是否可被捕獲 | 取決于發送的信號 | 取決于發送的信號 | 可被捕獲但無法真正阻止終止 |
底層關聯 | kill ?系統調用的封裝 | 通常用?kill(getpid(), sig) ?實現 | 內部使用?raise(SIGABRT) ?并確保進程死亡 |
核心要點:
使用?
kill()
?進行精細的進程控制。使用?
raise()
?進行簡潔的自我信號觸發。使用?
abort()
?作為處理不可恢復錯誤的最后手段,通常緊隨類似?assert()
?的檢查之后。
2.2 硬件異常
核心概念:硬件異常 -> 內核 -> 信號
這個過程并不像“快遞員主動打電話”,而更像“你在拆快遞時突然被包裝盒里的機關扎傷了手”——傷害是在執行操作的過程中由硬件直接檢測并立即報告的。
其核心流程如下:
CPU 執行指令:進程在執行一條指令。
硬件檢測異常:CPU 在執行過程中檢測到一個錯誤條件(如除以零、訪問非法地址)。
陷入內核:CPU 中止當前指令的執行,保存現場,并切換到內核模式,將控制權交給內核的陷阱處理程序。
內核處理陷阱:內核的陷阱處理程序檢查異常原因。
內核發送信號:內核將異常原因映射為一個對應的信號。
信號交付:內核將這個信號發送給導致異常的當前正在運行的進程。
這個過程是同步的:信號的產生是由進程自己的某條特定指令直接導致的,而不是像?kill
?或?Ctrl+C
?那樣是外部異步事件。
核心機制與流程
硬件檢測階段
- 觸發源:CPU 運算單元、內存管理單元(MMU)、浮點運算單元(FPU)等硬件組件。
- 異常類型:
- 除零錯誤:CPU 檢測到除法指令分母為 0(如?
x = 5 / 0
),觸發算術異常。 - 非法內存訪問:MMU 檢測到訪問未分配內存或越界地址(如解引用?
NULL
?指針)。 - 其他硬件錯誤:如無效指令、對齊錯誤、設備故障等。
- 除零錯誤:CPU 檢測到除法指令分母為 0(如?
- 硬件行為:
- 設置狀態寄存器標志位(如 x86 的?
#DE
(Divide Error)異常碼)。 - 向內核發送中斷請求(IRQ),攜帶異常類型和上下文信息。
- 設置狀態寄存器標志位(如 x86 的?
內核響應階段
- 異常解釋:內核接收中斷后,根據硬件提供的信息生成對應信號:
硬件異常 | 內核生成信號 | 信號含義 |
---|---|---|
除零/算術溢出 | SIGFPE ?(8) | 浮點或算術異常 |
非法內存訪問 | SIGSEGV ?(11) | 段錯誤(無效內存引用) |
總線錯誤(對齊問題) | SIGBUS ?(7) | 內存訪問對齊錯誤 |
- 信號注入:內核將信號加入目標進程的?未決(Pending)隊列,標記為待處理狀態。
- 進程處理階段
- 遞達時機:當進程從內核態返回用戶態時(如系統調用結束),檢查未決信號。
- 默認行為:
SIGFPE
/SIGSEGV
:終止進程并生成 core dump(內存轉儲文件)。SIGBUS
:終止進程。
- 自定義處理:進程可通過?
signal()
?或?sigaction()
?注冊處理函數覆蓋默認行為。
常見的硬件異常信號及詳解
以下是三種最典型的由硬件異常產生的信號:
1.?SIGFPE
?(信號 8) - 浮點異常
硬件根源:由 CPU 的算術邏輯單元 (ALU)?在執行算術運算時檢測到錯誤。
觸發原因:
整數除以零:這是最常見的原因。
浮點數除以零:可能產生?
Inf
?或?NaN
,但在某些上下文或系統配置下也會觸發信號。數值溢出:例如,對一個有符號整數進行運算,結果超出了其數據類型能表示的范圍。
示例代碼:
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>void handlerSig(int sig)
{std::cout << "獲得了一個信號: " << sig << std::endl;exit(12);
}int main()
{for(int i = 1; i < 32; i++)signal(i, handlerSig);while (true){std::cout << "I am a process, pid: " << getpid() << std::endl;sleep(1);int a = 10;a /= 0; // 除0錯誤}return 0;
}
運行結果:
這里我們直接忽略編譯報警,直接運行可以看到捕獲8號信號后退出
2.?SIGSEGV
?(信號 11) - 段錯誤
硬件根源:由 CPU 的內存管理單元 (MMU)?在執行內存訪問時檢測到錯誤。
觸發原因:
訪問空指針 (NULL):解引用?
0x0
?地址。訪問未分配的內存:解引用一個隨機的、無效的指針值。
訪問只讀內存:嘗試向代碼段(
.text
)或字符串常量(如?char *p = "hello"; p[0] = 'H';
)寫入數據。棧溢出:或者訪問了棧保護頁(Stack Guard Page)。
緩沖區溢出:訪問了數組邊界之外的內存。
示例代碼:
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>void handlerSig(int sig)
{std::cout << "獲得了一個信號: " << sig << std::endl;exit(12);
}int main()
{for(int i = 1; i < 32; i++)signal(i, handlerSig);while (true){std::cout << "I am a process, pid: " << getpid() << std::endl;sleep(1);int* p = NULL;*p = 100; // 野指針}return 0;
}
運行結果:
發生段錯誤,我們也成功捕獲到了11號信號
3.?SIGILL
?(信號 4) - 非法指令
硬件根源:由 CPU 的指令解碼單元檢測到錯誤。
觸發原因:
執行了損壞的二進制代碼:程序文件在磁盤或內存中被破壞。
嘗試執行數據:例如,函數指針指向了一個數據區而非代碼區。
CPU架構不匹配:嘗試在一種CPU上運行為另一種CPU編譯的二進制程序。
使用了特權指令:用戶態進程嘗試執行只有內核態才能執行的指令。
操作系統是怎么檢測到硬件異常?或者說硬件是怎么檢測到出錯的?
核心機制:CPU 的“異常處理”硬件單元
現代 CPU 并不是傻傻地執行指令,它的內部有一套復雜的監控電路。當這些電路在執行指令的過程中檢測到某些特定條件時,會立即中斷當前控制流,并強制 CPU 去執行一段預設好的、屬于操作系統的代碼。這個過程是硬件自動完成的。
我們可以用以下流程圖來概括這個硬協同的過程:
下面,我們以最常見的?SIGSEGV
?(段錯誤)?和?SIGFPE
?(除零錯誤)?為例,拆解圖中的每一步。
1. 硬件層面:檢測與觸發
對于?SIGSEGV
?(非法內存訪問):
關鍵硬件:MMU (內存管理單元)
CPU 的核心部件之一,負責將進程使用的虛擬地址翻譯為實際的物理地址。
檢測過程:
當執行一條像?
movl $100, (%eax)
(假設?eax=0
)的指令,試圖向地址 0 寫入時,CPU 會將虛擬地址?0
?交給 MMU。MMU 會查詢當前進程的頁表(Page Table)。頁表是內核數據結構,定義了虛擬地址到物理地址的映射關系以及訪問權限(是否可讀、可寫、可執行)。
MMU 發現虛擬地址?
0
?在頁表中根本沒有有效的映射,或者它的權限是只讀的(比如嘗試寫入代碼段)。MMU 立刻會產生一個?Page Fault(缺頁錯誤)或?General Protection Fault(通用保護錯誤)。這就是一個硬件異常。
對于?SIGFPE
?(算術異常):
關鍵硬件:ALU (算術邏輯單元)
CPU 的核心部件,負責執行所有算術和邏輯運算。
檢測過程:
當執行一條?
idivl
?指令(整數除法)時,如果除數是 0,ALU 中的除法電路會直接檢測到這個非法操作。除法電路立即產生一個?Divide Error?Fault。這同樣是一個硬件異常。
2. 內核層面:接管與處理
硬件只負責“發現問題并拍下緊急剎車”,接下來必須由操作系統內核來“處理事故現場”。
中斷描述符表 (IDT):
CPU 在設計之初就和操作系統約定好:發生不同類型的異常時,你應該去哪段代碼找我。這個約定就是 IDT。IDT 是由操作系統在啟動時精心設置的,它將每一種異常(如 Page Fault, Divide Error)和一個特定的內核函數地址(稱為中斷服務例程或陷阱處理程序)關聯起來。
內核的陷阱處理程序 (Trap Handler):
接管控制權:當 CPU 觸發上述硬件異常時,它會根據異常類型自動查找 IDT,并立即跳轉到對應的內核陷阱處理程序代碼執行。此時,CPU 從用戶態切換到了內核態。
保存現場:CPU 會自動將當時的寄存器狀態、指令指針(EIP/RIP)等壓入內核棧,這樣之后才能恢復。
分析原因:內核代碼開始分析異常原因。對于 Page Fault,它會檢查出錯的地址和錯誤類型。
判斷與決策:
可修復錯誤:有些 Page Fault 是正常的,比如訪問一個已被換出到硬盤的內存頁(按需分頁)。內核會默默地修復這個問題(從硬盤把頁換回內存,建立映射),然后讓進程重新執行剛才那條指令,進程對此毫無感知。
不可修復錯誤:如果內核發現這個錯誤無法修復(比如訪問了根本不存在的地址、權限錯誤、除以零),它就會認定是進程自己犯了嚴重的錯誤。
3. 信號傳遞:內核 -> 用戶進程
當內核斷定這是一個由用戶進程導致的、無法恢復的錯誤時,它就會動用信號機制來“通知”進程。
映射異常到信號:內核中有一個固定的映射關系:
Page Fault
?/?General Protection Fault
?->?SIGSEGV
Divide Error
?->?SIGFPE
Illegal Instruction
?->?SIGILL
發送信號:內核會直接修改當前出錯進程的?
task_struct
?結構體,在其待處理信號集(pending)?中設置對應的信號位(比如設置?SIGSEGV
?位為 1)。返回用戶態并交付信號:當內核的陷阱處理程序執行完畢,準備返回用戶態讓進程繼續執行時,它會檢查當前進程是否有待處理的信號。一檢查,發現有一個?
SIGSEGV
?pending,于是它不會返回進程原來出錯的那條指令,而是轉而執行進程注冊的?SIGSEGV
?信號處理函數。如果進程沒有注冊自定義處理函數,就執行默認動作(終止并生成 core dump)。
總結:這不是“檢測”,而是“報告”和“處理”
硬件 (CPU):就像一個嚴格執行命令但又嚴格遵守規則的工人。它的職責是報告:“老板,你讓我做的這個操作,根據你(操作系統)給我的規則,我沒法執行!”
操作系統內核:就像是工地的總包項目經理。它制定了規則(頁表),并負責處理工人的報告。它能修的小問題就自己修了(缺頁異常),修不了的嚴重問題就通知具體的小包工頭(用戶進程):“你手下的工人犯了致命錯誤,你自己看著辦吧(發送信號)。”
用戶進程:就是那個小包工頭。它提前告訴項目經理:“如果我的工人出了那種錯,你就叫我這個處理函數(signal handler)”。
所以,整個過程是:CPU硬件電路在運行中自動觸發異常 -> 內核預設的陷阱處理程序接管 -> 內核將異常類型轉換為信號 -> 內核將信號注入目標進程 -> 進程在合適時機處理信號。
Core Dump
在前面章節介紹進程等待時,我們在獲取子進程status時,有見過Core Dump(如下圖),那也是我們第一次知道Core Dump,當然我們當時只知道有這個東西,但不知道也不了解它,在前文中我們也能見到有的信號的默認動作是Term,有的信號默認動作則是Core,下面我們就來了解一下Core Dump。
一、Core Dump 是什么?
Core Dump(核心轉儲),在 Linux 和類 Unix 系統中,是指當進程異常終止(崩潰)時,操作系統將該進程在崩潰瞬間的整個用戶空間內存內容(以及部分內核數據結構)完整地保存到一個磁盤文件中的過程。生成的這個文件通常命名為?core
?或?core.<pid>
。
你可以把它想象成進程的?“死亡現場的快照”?或?“黑匣子”。它完整記錄了進程在“死亡”那一刻的:
內存數據:堆(heap)、棧(stack)、數據段(data segment)、BSS 段。
寄存器狀態:程序計數器(PC)、棧指針(SP)等,這直接指向了崩潰時正在執行的代碼。
程序計數器值:明確指出是哪條指令導致了崩潰。
內存管理信息:頁表、文件描述符表等資源信息。
二、為什么會產生 Core Dump?
Core Dump 主要由一些特定的信號觸發,這些信號的默認行為是?Core
。常見的觸發信號有:
信號 | 編號 | 原因 | 默認行為 |
---|---|---|---|
SIGQUIT | 3 | 用戶按下?Ctrl+\ | Core |
SIGILL | 4 | 執行了非法指令 | Core |
SIGABRT | 6 | 程序自己調用?abort() ?函數 | Core |
SIGFPE | 8 | 算術異常,如除以零 | Core |
SIGSEGV | 11 | 段錯誤,非法內存訪問(最最常見的原因!) | Core |
當一個進程收到上述信號,并且沒有捕獲它或者捕獲后依然決定終止,操作系統就會執行默認動作:終止進程并生成 core dump。
三、Core Dump 有什么用?(為什么它如此重要?)
核心用途:事后調試(Post-mortem Debugging)
程序員不可能 7x24 小時盯著程序運行。很多崩潰(尤其是段錯誤?SIGSEGV
)是隨機發生的,在測試環境中難以復現。Core Dump 文件提供了重現崩潰現場的一切信息。借助調試器(如 GDB),你可以:
精確定位崩潰位置:直接看到崩潰時程序執行到了哪一行代碼、哪個函數。
查看調用棧(Backtrace):看到函數調用的完整鏈條,了解是如何一步步走到崩潰點的。
檢查變量值:查看在崩潰瞬間,各個全局變量、局部變量的值是什么,這對于分析邏輯錯誤至關重要。
分析內存狀態:檢查指針是否為空、是否被釋放、數組是否越界等。
沒有 core dump,調試這種崩潰就如同刑偵破案沒有監控錄像和物證,只能靠猜測和打印日志,效率極低。
四、如何啟用和配置 Core Dump?
默認情況下,很多系統為了節省磁盤空間,core dump 功能是關閉的。你需要進行配置。
1. 解除資源限制:ulimit -c
Shell 內置命令?ulimit
?用于控制 shell 啟動的進程所占用的資源。
檢查當前限制:
$ ulimit -c 0 # 如果結果是 0,表示禁止生成 core 文件
設置 core 文件大小限制:
$ ulimit -c unlimited # 設置為無限制(最常用) # 或者指定大小(單位是 KB) $ ulimit -c 102400 # 設置最大為 100MB
注意:這個設置只對當前終端會話有效。要永久生效,需要將?
ulimit -c unlimited
?添加到你的?~/.bashrc
?或?/etc/profile
?等配置文件中。
2. 配置 Core 文件名稱和路徑:/proc/sys/kernel/core_pattern
core_pattern
?文件決定了 core 文件的生成位置和命名方式。
查看當前設置:
$ cat /proc/sys/kernel/core_pattern
可能是簡單的?
core
,也可能是?|/usr/share/apport/apport %p %s %c %d %P
(像 Ubuntu 就使用?apport
?來管理 core dump)。自定義設置(需要 root 權限):
# 將 core 文件生成到 /var/cores/ 目錄下,并以 core-pid-timestamp 的格式命名 $ sudo echo "/var/cores/core-%p-%t" > /proc/sys/kernel/core_pattern# 確保目錄存在且有寫入權限 $ sudo mkdir /var/cores $ sudo chmod 777 /var/cores # 或者設置為一個更安全的權限
常用格式符:
%p
:進程 ID (PID)%u
:用戶 ID%t
:時間戳 (Unix epoch)%s
:導致 dump 的信號編號%e
:可執行文件名
五、如何使用 Core Dump 進行調試?
假設你的程序?my_program
?崩潰并生成了一個?core
?文件。
使用 GDB 進行分析:
# 基本命令格式:gdb <可執行程序> <core文件>
$ gdb my_program core# 或者如果 core 文件有復雜的名字
$ gdb my_program /var/cores/core-12345-1620000000
進入 GDB 后,最關鍵的幾個命令:
bt
?(backtrace):立即查看調用棧。這是你第一個應該執行的命令。它會顯示出崩潰時函數調用的層次關系,直接指向問題代碼。(gdb) bt #0 0x0000000000400556 in foo () at main.c:10 #1 0x0000000000400582 in main () at main.c:20
這清楚地告訴我們:在?
main.c
?的第 20 行,main
?函數調用了?foo
?函數,然后在?main.c
?的第 10 行,foo
?函數內部發生了崩潰。f <幀號>
?(frame):切換到調用棧的某一具體幀,查看該層的上下文。(gdb) f 0 # 切換到第0幀(崩潰發生的地方) (gdb) list # 查看崩潰點附近的代碼
p <變量名>
?(print):打印變量的值。這對于檢查指針是否為?NULL
?或變量值是否符合預期至關重要。(gdb) p ptr $1 = (int *) 0x0 # 啊哈!發現一個空指針!
info registers
:查看寄存器的值。
六、注意事項與最佳實踐
編譯時請帶上?
-g
?選項:在編譯你的程序時(gcc -g -o my_program my_program.c
),-g
?選項會在可執行文件中包含調試符號信息。如果沒有這個信息,GDB 只能告訴你崩潰的機器指令地址,而無法告訴你對應的源代碼文件名和行號,調試難度大大增加。確保權限和磁盤空間:進程要對?
core_pattern
?指定的目錄有寫入權限,并且磁盤有足夠空間。生產環境:在生產服務器上,通常不會設置?
ulimit -c unlimited
,因為 core 文件可能非常大(幾個GB),填滿磁盤會導致更嚴重的問題。生產環境的做法通常是:使用?
core_pattern
?將 core 文件重定向到一個有充足空間、專門用于監控的目錄。或者集成更高級的監控系統,在崩潰時自動捕獲 core 文件并上傳到中央服務器進行分析,然后刪除本地的文件。
2.3 軟件條件
這類信號的特點是:它們并非由外部進程或用戶通過?kill
?發送,也非由硬件錯誤觸發,而是由操作系統內核在檢測到某種特定的、預先定義的“軟件條件”滿足時,自動向進程發送的。
常見的軟件條件信號及詳解
以下是幾個最典型的由軟件條件產生的信號:
1.?SIGPIPE
?(信號 13) - 管道破裂
這是最經典的“軟件條件”信號。
觸發條件:當一個進程試圖向一個已經沒有任何讀者的管道(pipe)、FIFO(命名管道)或套接字(socket)?進行寫入操作時,內核會自動向這個寫入進程發送?
SIGPIPE
?信號。為什么需要它??這是一種“斷連”通知。想象一下,你用?
pipe
?創建了一個管道,進程A讀,進程B寫。如果進程A意外退出了,進程B還在不停地寫,這些數據將永遠無人讀取,寫操作也就失去了意義。內核通過?SIGPIPE
?來強行阻止這種無意義的操作。默認行為:終止進程。
如何處理:很多時候,我們并不希望寫入進程因為讀端關閉就直接崩潰。因此,一個常見的做法是忽略?
SIGPIPE
?信號(signal(SIGPIPE, SIG_IGN);
)。這樣,當寫入發生時,系統調用(如?write()
)不會導致進程終止,而是會返回?-1
?并設置錯誤碼?errno
?為?EPIPE
。程序可以通過檢查返回值來進行更優雅的錯誤處理。
我們在進程間通信中介紹管道時,有詳細介紹這種情況,所以這里不過多介紹
2.?SIGALRM
?(信號 14) - 定時器信號
這是一個由軟件定時器超時這一條件觸發的信號。
觸發條件:當一個由?
alarm()
?或?setitimer()
?函數設置的實時定時器(Real-time Timer)超時后,內核會向調用該定時器的進程發送?SIGALRM
?信號。為什么需要它??用于實現超時機制和周期性任務。例如,設置一個讀寫操作的超時時間,或者讓一個任務每隔一段時間執行一次。
默認行為:終止進程。
如何使用:進程通常會捕獲?
SIGALRM
?并提供一個處理函數。在處理函數中設置一個標志位,主程序通過檢查這個標志位來判斷是否超時。
alarm
?系統調用詳解
1. 函數原型與功能
#include <unistd.h>unsigned int alarm(unsigned int seconds);
功能:設置一個實時定時器(也叫“鬧鐘”)。這個定時器會在指定的秒數后到期。當定時器到期時,內核會向調用進程發送一個?
SIGALRM
?信號。參數:
seconds
?- 指定定時器到期的時間,單位是秒。如果?seconds
?為?0
,則表示取消之前設置的所有尚未觸發的?alarm
?定時器。返回值:
返回之前設置的鬧鐘還剩余的秒數。
如果之前沒有設置過鬧鐘,則返回?
0
。
2. 關鍵特性與工作機制
單一定時器:對于一個進程,
alarm
?調用只維護一個定時器。新的?alarm
?調用會覆蓋之前設置的定時器。示例:
alarm(10); // 設置一個10秒后觸發的定時器 sleep(2); // 等待2秒 unsigned int remaining = alarm(5); // 設置一個新的5秒定時器 // remaining 的值將是 8 (10 - 2 = 8) // 現在,舊的10秒定時器被取消了,取而代之的是一個5秒后觸發的定時器。
信號交付:定時器到期后,內核向進程發送?
SIGALRM
?信號。該信號的默認行為是終止進程。這意味著,如果你只是調用?alarm(5)
?而不做任何處理,你的進程將在5秒后默默退出。異步性:信號的產生和處理是異步的。定時器到期可能發生在進程執行流中的任何一點。
精度:
alarm
?的精度是秒級,這對于需要更高精度(毫秒、微秒)定時任務的場景來說太粗糙了。
示例:
我們可以通過鬧鐘來驗證一下IO效率問題
我們先看一下一秒鐘內,不停IO可以輸出多少次
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>void handlerSig(int sig)
{std::cout << "獲得了一個信號: " << sig << std::endl;exit(1);
}int cnt = 0;
int main()
{signal(SIGALRM, handlerSig);alarm(1);while (true){std::cout << "count: " << cnt << std::endl;cnt++;}return 0;
}
運行結果:
可以看到不停IO輸出,可以一秒鐘打印76136次,那如果我們在一秒鐘內只讓cnt++,但只在鬧鐘結束之后IO一次輸出cnt的值,這種情況會是多少呢?
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>int cnt = 0;void handlerSig(int sig)
{std::cout << "獲得了一個信號: " << sig << "count: " << cnt << std::endl;exit(1);
}int main()
{signal(SIGALRM, handlerSig);alarm(1);while (true){cnt++;}return 0;
}
運行結果:
可以看到輸出結果為4億多
IO的本質: xshell->./XXX->云服務器->網絡->我們看到
如果我們不停IO,這些過程也會不停重復,所以效率就會要慢很多
我們還可以設置一個鬧鐘每隔一秒發送一次信號,然后捕獲
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>void handlerSig(int sig)
{std::cout << "獲得了一個信號: " << sig << std::endl;alarm(1);
}int main()
{signal(SIGALRM, handlerSig);alarm(1);while (true){std::cout << "I am a process, pid: " << getpid() << std::endl;sleep(1);}return 0;
}
運行結果:
可以看到,鬧鐘每次時間到了就會發送一個14號信號
3.?SIGCHLD
?(信號 17) - 子進程狀態改變
這是一個極其重要的、由內核自動管理的信號。
觸發條件:當一個進程的子進程終止或停止(例如被?
SIGSTOP
?暫停),又或者從停止狀態恢復繼續運行時,內核會自動向父進程發送?SIGCHLD
?信號。為什么需要它??這是一種通知機制,告訴父進程:“你的一個子進程的狀態變了,你該來處理一下了(比如回收資源)”。它解決了父進程如何高效地知道子進程結束的問題,避免了父進程不斷輪詢調用?
wait()
(忙等待)。默認行為:忽略(
SIG_IGN
)。但請注意,默認忽略和手動忽略在底層有巨大差別。如何響應:父進程必須捕獲?
SIGCHLD
?信號,并在其處理函數中調用?wait()
?或?waitpid()
?來回收子進程資源,從而防止出現僵尸進程(Zombie Process)。
這里也不過多介紹,進程等待部分介紹過
總結:軟件條件信號的特點
信號 | 觸發條件 | 核心用途 | 常見處理方式 |
---|---|---|---|
SIGPIPE | 向無讀者的通道寫入 | 處理斷裂的管道/Socket連接 | 忽略,并檢查?write ?返回值 |
SIGALRM | 定時器超時 | 實現超時、輪詢、周期性任務 | 捕獲,在handler中設置標志位 |
SIGCHLD | 子進程狀態改變 | 異步通知父進程回收子進程資源 | 捕獲,并在handler中調用?waitpid |
核心思想:
這些信號體現了操作系統內核的一種設計哲學:“讓我來替你盯著這些繁瑣的事情,當條件發生時,我會主動通知你”。這極大地簡化了應用程序的設計,使其從同步輪詢的負擔中解脫出來,轉向異步事件驅動的高效模型。
3. 保存信號
介紹完信號的產生,那么接下來就要來介紹一下信號的保存
3.1 信號相關概念詳解
在前文通過快遞引入信號概念后,我們總結了一個基本結論,其中涉及到的一些概念,我們并不明白其中的含義,下面我們就來了解一下
信號遞達 (Delivery)
信號遞達指的是操作系統實際執行信號處理程序的過程。當信號遞達時,系統會根據以下三種可能的處理方式之一來響應:
- 默認處理:執行系統預定義的操作(如終止進程)
- 忽略處理:完全丟棄該信號
- 自定義處理:執行用戶注冊的信號處理函數
例如,當進程收到SIGINT信號(Ctrl+C)時,默認處理方式是終止進程,但如果用戶注冊了處理函數,則會執行該函數。
信號未決 (Pending)
信號從產生到遞達之間會經歷未決狀態。這個過程中:
- 信號被記錄在進程的未決信號集合中
- 每個信號都有一個對應的未決標志位
- 信號可能因為阻塞而長時間保持未決狀態
信號阻塞 (Block)
進程可以通過信號掩碼主動阻塞某些信號:
- 阻塞的信號仍可被接收,但不會立即遞達
- 被阻塞的信號會一直保持在未決狀態
- 常見阻塞場景包括:
- 關鍵代碼段執行期間
- 信號處理函數執行時
- 進程初始化階段
阻塞與忽略的區別
特性 | 阻塞 (Block) | 忽略 (Ignore) |
---|---|---|
作用時機 | 信號遞達前 | 信號遞達后 |
信號狀態 | 保持未決 | 已被處理 |
后續影響 | 解除阻塞后會遞達 | 直接丟棄 |
典型應用 | 臨時屏蔽關鍵信號 | 永久忽略無關信號 |
例如,在銀行交易處理中:
- 阻塞SIGINT可防止交易中途被中斷
- 忽略SIGCHLD可避免處理子進程狀態變化
3.2 信號在內核中的表示
內核中通過三張表來表示信號
三張核心表
1. 信號處理動作表 (sighand_struct->action[]
)
位置:
task_struct->sighand->action[64]
作用:定義了對每個信號的處理方式
大小:
_NSIG
(通常為64),對應64種可能的信號內容:每個元素是一個
k_sigaction
結構,包含:sa_handler
:信號處理函數指針(可以是SIG_DFL
、SIG_IGN
或用戶自定義函數)sa_flags
:控制信號處理的各種標志sa_mask
:在執行此信號處理函數時,需要阻塞的其他信號集sa_restorer
:恢復函數(通常不由應用程序直接使用)
2. 阻塞信號表 (blocked
)
位置:
task_struct->blocked
作用:記錄當前被進程阻塞(屏蔽)的信號
類型:
sigset_t
(一個位掩碼,每位對應一個信號)功能:即使信號產生,如果它在阻塞集中,也不會被遞送給進程,直到解除阻塞
3. 未決信號表 (pending
)
位置:
task_struct->pending
作用:記錄已經產生但尚未遞達(處理)的信號
結構:包含一個
sigset_t
(位圖)和一個list_head
(鏈表)特殊功能:對于實時信號,
list_head
用于實現信號隊列,可以存儲多個相同的信號
信號處理示例分析
我們通過上圖中的例子來解釋這三種表如何協同工作:
SIGHUP 信號(信號1)
阻塞位:0(未阻塞)
未決位:0(未產生)
處理動作:默認處理動作(
SIG_DFL
)行為:當SIGHUP信號產生時,內核會設置未決標志,然后在合適的時候執行默認處理動作
SIGINT 信號(信號2)
阻塞位:1(被阻塞)
未決位:1(已產生但未處理)
處理動作:忽略(
SIG_IGN
)行為:雖然處理動作是忽略,但由于信號被阻塞,它暫時不能被處理。進程有機會在解除阻塞前改變處理動作
SIGQUIT 信號(信號3)
阻塞位:1(被阻塞)
未決位:0(未產生)
處理動作:用戶自定義函數
sighandler
行為:一旦產生SIGQUIT信號,它將被阻塞,直到解除阻塞后才會調用
sighandler
關鍵機制:信號阻塞與未決
阻塞與未決的關系
信號產生時,內核首先檢查該信號是否被阻塞
如果未被阻塞,內核可能直接遞送信號(取決于信號類型和當前狀態)
如果被阻塞,內核設置該信號的未決標志,但不立即遞送
當進程解除對某信號的阻塞時,內核檢查該信號的未決標志
如果未決標志被設置,內核隨后會遞送該信號
常規信號 vs 實時信號
常規信號(1-31):在遞達之前產生多次只計一次,會丟失額外的信號
實時信號(34-64):支持排隊,多次產生的信號會依次存放在隊列中,不會丟失
內核數據結構詳解
// 進程描述符中與信號相關的字段
struct task_struct {// ...struct sighand_struct *sighand; // 指向信號處理表sigset_t blocked; // 阻塞信號表(位圖)struct sigpending pending; // 未決信號表// ...
};// 信號處理表結構
struct sighand_struct {atomic_t count; // 引用計數struct k_sigaction action[_NSIG]; // 每個信號的處理動作spinlock_t siglock; // 保護該結構的自旋鎖
};// 信號處理動作詳情
struct k_sigaction {struct __new_sigaction sa; // 信號處理結構void __user *ka_restorer; // 恢復函數指針
};// 信號處理結構
struct __new_sigaction {__sighandler_t sa_handler; // 信號處理函數指針unsigned long sa_flags; // 標志位void (*sa_restorer)(void); // 恢復函數(通常不使用)__new_sigset_t sa_mask; // 執行處理函數時要阻塞的信號集
};// 未決信號結構
struct sigpending {struct list_head list; // 實時信號的隊列sigset_t signal; // 未決信號的位圖
};
總結
Linux內核通過三張表精細地管理信號:
處理動作表決定了信號最終如何被處理
阻塞表控制哪些信號暫時不被處理
未決表記錄已產生但尚未處理的信號
3.3?sigset_t和信號集操作函數
?sigset_t:信號集
1. 本質:一個位掩碼(Bitmask)
sigset_t
?是一個不透明的數據類型,通常在內核中定義為一個大整數或整數數組。它的每一位(bit)對應一個信號編號。
例如,第 1 位代表信號 1 (
SIGHUP
),第 2 位代表信號 2 (SIGINT
),以此類推。
位的值只有兩種狀態:
1
(有效):表示該信號處于“有效”狀態。0
(無效):表示該信號處于“無效”狀態。
2. 兩種角色,兩種含義
sigset_t
?類型的變量在不同的上下文中扮演不同角色,因此相同的“有效”狀態有著截然不同的含義:
上下文 | 集合名稱 | “有效”(bit = 1)的含義 | “無效”(bit = 0)的含義 |
---|---|---|---|
阻塞信號集 | 阻塞集/屏蔽集 | 該信號被當前進程阻塞(Blocked) | 該信號未被阻塞 |
未決信號集 | 未決集 | 該信號已產生,但尚未遞達(處理),即處于未決(Pending)狀態 | 該信號未產生或已處理(未決標志已清除) |
關鍵區別:
阻塞集是一個設置。它由進程主動通過系統調用(如?
sigprocmask
)來設定,表示“我不想現在接收這些信號”。未決集是一個記錄。它由內核自動維護,表示“這些信號已經送達門口,但還沒被處理”。
3. 為什么“屏蔽”應理解為“阻塞”而不是“忽略”?
這是一個非常關鍵的概念區分:
忽略(Ignore):是一種信號處理動作。當信號已遞達時,進程選擇不做任何操作。它通過?
signal(sig, SIG_IGN)
?來設置。阻塞/屏蔽(Block):是一種信號遞達前的狀態管理。它阻止信號被遞達,信號會一直保持在未決狀態,直到解除阻塞。它通過?
sigprocmask
?等函數操作阻塞集來實現。
一個信號的旅程:
產生 -> (檢查阻塞集?是:進入未決集;否:準備遞達) -> 遞達 -> (檢查處理動作:默認、忽略、捕獲)
舉個例子:
假設你對?SIGINT
?的處理動作是“忽略”(SIG_IGN
),但同時你又阻塞了?SIGINT
。
當?
SIGINT
?產生時,因為它被阻塞,所以不會立即遞達,而是先掛在未決集里。在此期間,你有機會將處理動作從“忽略”改為“自定義處理函數”。
當你解除對?
SIGINT
?的阻塞時,內核發現它未決,于是開始遞達。此時,內核才會去看處理動作表,發現是“忽略”,于是直接清除其未決位,什么都不做。
如果在第 2 步你沒有改變處理動作,那么最終結果看起來和直接“忽略”沒區別。但阻塞為你提供了一個改變決策的機會窗口,這是純粹的“忽略”所不具備的。
信號集操作函數
因為?sigset_t
?是不透明類型,你不能直接對其使用位操作(如?&
,?|
)。POSIX 定義了一套標準函數來操作它。
1. 初始化與基本操作
#include <signal.h>int sigemptyset(sigset_t *set); // 初始化set為空集合(所有位設為0)
int sigfillset(sigset_t *set); // 初始化set為包含所有信號的集合(所有位設為1)
int sigaddset(sigset_t *set, int signum); // 將指定信號signum添加到set中
int sigdelset(sigset_t *set, int signum); // 從set中刪除指定信號signum
int sigismember(const sigset_t *set, int signum); // 判斷信號signum是否在set中
這些函數成功返回?0
,失敗返回?-1
。
示例:創建一個只包含?SIGINT
?和?SIGQUIT
?的信號集。
sigset_t my_set;
sigemptyset(&my_set); // 必須先初始化為空!
sigaddset(&my_set, SIGINT);
sigaddset(&my_set, SIGQUIT);
其實和我們學習哈希擴展——位圖時的操作,在本質上是差不多的
2. 核心應用:修改進程信號屏蔽字(阻塞集)
#include <signal.h>int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
功能:讀取或更改進程的信號屏蔽字(阻塞集)。
參數?
how
:指定如何修改阻塞集。SIG_BLOCK
:阻塞?set
?中的信號。新屏蔽字 = 當前屏蔽字 |?set
。SIG_UNBLOCK
:解除阻塞?set
?中的信號。新屏蔽字 = 當前屏蔽字 & ~set
。SIG_SETMASK
:直接用?set
?替換當前屏蔽字。
參數?
set
:指向一個由之前?sigaddset
?等函數準備好的信號集。如果為?NULL
,則?how
?參數被忽略,函數只用于獲取舊的屏蔽字。參數?
oldset
:用于保存舊的信號屏蔽字,以便后續恢復。如果為?NULL
?則不保存。返回值:成功返回?
0
,失敗返回?-1
。
示例:阻塞?SIGINT
?信號。
sigset_t new_set, old_set;
sigemptyset(&new_set);
sigaddset(&new_set, SIGINT);// 阻塞SIGINT,并保存舊的屏蔽字到old_set
if (sigprocmask(SIG_BLOCK, &new_set, &old_set) == -1) {perror("sigprocmask");
}
// ... 在這段代碼中,SIGINT信號會被阻塞 ...
// 恢復舊的屏蔽字(解除對SIGINT的阻塞)
if (sigprocmask(SIG_SETMASK, &old_set, NULL) == -1) {perror("sigprocmask");
}
3. 獲取當前未決信號集
#include <signal.h>int sigpending(sigset_t *set);
功能:獲取當前進程的未決信號集,并通過?
set
?參數返回。參數?
set
:輸出型參數,用于存放獲取到的未決信號集。返回值:成功返回?
0
,失敗返回?-1
。
示例:檢查?SIGINT
?是否處于未決狀態。
sigset_t pending_set;
sigpending(&pending_set); // 獲取未決集
if (sigismember(&pending_set, SIGINT)) {printf("SIGINT is pending!\\n");
}
總結
sigset_t
?是一個位掩碼,用于表示信號的集合。它在阻塞集中表示“是否被屏蔽”,在未決集中表示“是否已產生但未處理”。
“阻塞”?和?“忽略”?是截然不同的概念:阻塞是遞達前的延遲,忽略是遞達后的處理動作。
必須使用?
sigemptyset
,?sigaddset
,?sigprocmask
?等標準函數來操作?sigset_t
,不能直接進行位運算。
完整示例:
void PrintPending(sigset_t& pending)
{std::cout << "我是一個進程, pid: " << getpid() << ", pending: ";for(int signo = 31; signo >= 1; signo--){if(sigismember(&pending, signo)){std::cout << "1";}else{std::cout << "0";}}std::cout << std::endl;
}int main()
{// 1. 初始化sigset_t block, oldblock;sigemptyset(&block);sigemptyset(&oldblock);// 2. 添加信號到阻塞表中for(int i = 31; i >= 1; i--)sigaddset(&block, i);// 3. 屏蔽信號int n = sigprocmask(SIG_SETMASK, &block, &oldblock);if(n < 0){perror("sigprocmask");}while(true){// 4. 獲取pending信號集合sigset_t pending;int m = sigpending(&pending);if(m < 0){perror("sigpending");}// 5. 打印pending信號集合PrintPending(pending);sleep(1);}return 0;
}
運行結果:
我們把所有普通信號的阻塞表都設為屏蔽,可以看到我們通過kill命令發送信號時,信號被屏蔽了,所以可以看到未決表中記錄的已產生但未遞達的信號集,我們發送一個信號,該信號集對應位置為1,但是由于9號信號比較特殊,無法被捕獲,阻塞和忽略,我們發送9號信號時,進程就被殺死了
下一篇文章我們再介紹信號的處理