個人主頁:chian-ocean
文章專欄-Linux
前言:
在 Linux 操作系統中,信號是用于進程間通信的一種機制,能夠向進程發送通知,指示某些事件的發生。信號通常由操作系統內核、硬件中斷或其他進程發送。接收和處理信號是 Linux 系統中進程控制和資源管理的一個重要組成部分。
信號的保存
信號遞送(Delivery)
- 信號的處理動作稱為信號遞送(Delivery)。
- 這意味著在 Linux 系統中,當信號發生時,它會被傳遞到目標進程并執行相應的操作。遞送是信號處理的第一步,是信號機制中至關重要的一部分。
2. 信號未決(Pending)
-
信號從產生到遞送之間的狀態,稱為信號未決(Pending)。
- 當信號發送到一個進程,但該進程因某些原因(如信號被屏蔽或進程正在執行其他操作)暫時無法處理信號時,這個信號就會處于未決狀態,等待后續處理。
-
Linux
內核是通過一個一個位圖編標記信號,進而儲存信號
block
位圖(通常是一個sigset_t
類型的數據結構)用于記錄當前進程被阻塞的信號。每一位代表一個特定的信號,當該位被設置為1時,表示該信號處于被阻塞狀態;如果該位是0,則表示該信號未被阻塞,可以遞送給進程。pending 位圖
(Pending Bitmap)用于記錄進程中待處理的信號。位圖是一個位數組,其中每個比特代表一個信號的狀態(是否待處理)。headler
代表的是該信號所需要用的方法。
sigset_t
定義:
sigset_t
是一個適用于信號管理的基本數據類型,通常在頭文件 <signal.h>
中定義。它的具體實現依賴于系統,但通常是一個位圖或位集合,每一位表示一個信號的狀態(是否被屏蔽、是否待處理等)。
sigset_t
的用途
- 信號屏蔽:用于設置進程的信號掩碼(signal mask),即哪些信號被屏蔽,不允許遞送。例如,通過
sigprocmask
函數修改信號掩碼。 - 信號檢查:通過
sigpending()
函數檢查進程是否有待處理的信號。 - 信號操作:例如,
sigaddset()
和sigdelset()
等函數可以用來向信號集添加或移除特定信號
常用函數與 sigset_t
的操作
在 Linux 系統中,sigset_t
是一個數據類型,用于表示信號集,通常用于管理信號的掩碼。它用于存儲一個進程中多個信號的集合,可以用來表示進程阻塞的信號、待處理的信號等。
sigset_t
類型定義
sigset_t
是一個適用于信號管理的基本數據類型,通常在頭文件 <signal.h>
中定義。它的具體實現依賴于系統,但通常是一個位圖或位集合,每一位表示一個信號的狀態(是否被屏蔽、是否待處理等)。
sigset_t
的用途
sigset_t
主要用于以下幾個方面:
- 信號屏蔽:用于設置進程的信號掩碼(signal mask),即哪些信號被屏蔽,不允許遞送。例如,通過
sigprocmask
函數修改信號掩碼。 - 信號檢查:通過
sigpending()
函數檢查進程是否有待處理的信號。 - 信號操作:例如,
sigaddset()
和sigdelset()
等函數可以用來向信號集添加或移除特定信號。
sigset_t
的操作
以下是一些與 sigset_t
相關的常見操作函數:
-
sigemptyset(sigset_t ,*set)
: 初始化一個空的信號集(即不包含任何信號)。sigset_t set; sigemptyset(&set); // 將 set 設為空信號集
-
sigfillset(sigset_t ,*set)
: 初始化一個包含所有信號的信號集。sigset_t set; sigfillset(&set); // 將 set 設為包含所有信號的信號集
-
sigaddset(sigset_t ,*set, int signo)
: 將指定的信號添加到信號集。sigset_t set; sigemptyset(&set); sigaddset(&set, SIGINT); // 將 SIGINT 添加到 set 中
-
sigdelset(sigset_t ,*set, int signo)
: 從信號集移除指定的信號。sigdelset(&set, SIGINT); // 從 set 中刪除 SIGINT
-
sigismember(const sigset_t ,*set, int signo)
: 檢查指定的信號是否在信號集中。if (sigismember(&set, SIGINT)) {// 如果 SIGINT 在 set 集合中 }
控制sigset_t
常見函數
sigprocmask(int how, const sigset_t ,*set, sigset_t ,*oldset)
: 修改進程的信號掩碼(即屏蔽哪些信號)。
-
sigismember(const sigset_t ,*set, int signo)
: 檢查指定的信號是否在信號集中。if (sigismember(&set, SIGINT)) {// 如果 SIGINT 在 set 集合中 }
-
how
:參數控制信號掩碼的設置方式:
SIG_BLOCK
:把 set中的信號添加到blocked
中(blocked= blocked | set)
。SIG_UNBLOCK
:從 blocked中刪除 set中的信號(blocked= blocked & set)
。SIG_SETMASK
:block= set
sigset_t set, oldset; sigemptyset(&set); sigaddset(&set, SIGINT); sigprocmask(SIG_BLOCK, &set, &oldset); // 阻塞 SIGINT
-
-
sigpending(sigset_t g*set)
: 獲取當前進程的未決信號集,返回一個信號集,表示所有尚未被處理的信號。sigset_t set; sigpending(&set); // 獲取當前進程的待處理信號
屏蔽字的實例
#include <iostream>
#include <signal.h>
#include <unistd.h>// 函數:用于打印信號集中的信號狀態
void pendingprint(sigset_t &pending)
{// 遍歷信號集中的每個信號,從31到1(Linux支持的信號范圍通常是1到31)for(int i = 31; i >= 1; i--){// 檢查信號集pending中是否包含第i個信號if (sigismember(&pending, i)){std::cout << "1"; // 如果信號存在,打印"1"}else{std::cout << "0"; // 如果信號不存在,打印"0"}}std::cout << "\n"; // 換行打印結果
}int main()
{// 定義信號集,用于保存信號掩碼、舊掩碼和待處理信號集sigset_t mask, old_mask;sigset_t pending;// 初始化信號集mask為空信號集sigemptyset(&mask);// 將SIGINT信號(Ctrl+C觸發的中斷信號)添加到信號集mask中sigaddset(&mask, SIGINT);// 將SIGINT信號添加到當前進程的信號掩碼中,即阻塞SIGINT信號// 并保存原來的信號掩碼到old_mask中sigprocmask(SIG_BLOCK, &mask, &old_mask);int cnt = 0;while(true){// 獲取當前進程的待處理信號集,并保存在pending中sigpending(&pending);// 調用pendingprint函數,打印待處理信號集中的信號pendingprint(pending);// 每次睡眠1秒鐘sleep(1);// 計數器,每次增加1if(++cnt == 5){// 在第20次循環時,解除阻塞SIGINT信號std::cout << "SIGINT UNLOCK" << std::endl;// 恢復之前的信號掩碼,即解除對SIGINT信號的阻塞sigprocmask(SIG_SETMASK, &old_mask, NULL);}}std::cout << "QUIT..." << std::endl;return 0;
}
- 代碼中我會每秒打印
penging
中的數據 - 第三秒中的時候,像進程發送2好信號,
penging
中會顯示為處理的信號,在bolck
中該信號被阻塞,也就是被屏蔽。 - 代碼執行第五秒中的時候,進程阻塞信號解除,由于
penging
中有未處理的信號,就會執行2好號信號。
信號的捕捉
信號捕捉的時機
- 進程是否正在執行系統調用。
- 進程是否在空閑狀態(如調用
sleep()
)。 - 信號是否被發送。
- 信號是否被阻塞或保留直到適當時機。
- 信號是否導致進程退出或崩潰時處理。
信號執行的流程
- 用戶模式:進程在用戶模式下執行,直到某個信號因中斷、異常或系統調用被觸發。
- 內核模式:當信號被觸發時,操作系統切換到內核模式,進行信號的處理。內核會完成信號的處理并準備將進程返回到用戶模式之前的狀態。
- 信號處理:在信號處理過程中,內核會調用信號處理函數(如
do_signal()
)。如果信號需要對用戶指定的特定處理,系統會在信號處理時調用相應的函數。 - 處理返回:信號處理函數執行完畢后,操作系統會通過
sigreturn
系統調用返回到用戶模式。 - 恢復執行:操作系統恢復用戶模式下的程序執行,從中斷的地方繼續執行。
用戶態和內核態的切換
用戶態到內核態:
- 觸發方式:通過系統調用(如
read()
,write()
)或硬件中斷(如鍵盤中斷)。 - 過程:
- 當用戶程序調用系統調用時,會執行一個軟中斷(例如x86的
int 0x80
指令)。 - 內核會保存當前用戶程序的上下文(如程序計數器、堆棧指針等),并設置內核模式。
- 進入內核代碼執行,執行完后,準備返回用戶態。
- 當用戶程序調用系統調用時,會執行一個軟中斷(例如x86的
內核態到用戶態:
- 觸發方式:內核任務完成后,需要返回用戶程序繼續執行。
- 過程:
- 內核將處理結果返回用戶進程,并恢復用戶進程的上下文。
- 恢復用戶態的堆棧指針、程序計數器等寄存器,并將程序從內核模式切換回用戶模式。
- 繼續執行用戶進程。
sigaction
sigaction
是處理粒度更加強大的一個系統調用。
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
參數:
signum
:指定信號的編號(例如,SIGINT
、SIGTERM
等)。act
:指向struct sigaction
結構體的指針,用于指定新的信號處理程序及其設置。oldact
:指向struct sigaction
結構體的指針,用于保存之前的信號處理程序(可選)。如果不關心舊的處理程序,可以傳遞NULL
。
返回值:
- 成功時,返回
0
。 - 失敗時,返回
-1
并設置errno
。
struct sigaction {void (*sa_handler)(int); // 信號處理函數void (*sa_sigaction)(int, siginfo_t *, void *); // 備用信號處理函數sigset_t sa_mask; // 用于指定在信號處理期間要阻塞的信號int sa_flags; // 信號處理行為的標志void (*sa_restorer)(void); // 不常用,保留字段
};
代碼示例:
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<cstring>
#include<cstdlib>// 信號處理函數,當捕獲到信號時執行
void headler(int signo)
{// 輸出捕獲到的信號類型std::cout << "catch signal --- signo :" << signo << std::endl;sleep(4);// 退出程序,返回 1 作為退出狀態exit(1);
}int main()
{ // 定義 sigaction 結構體,用于設置新的信號處理行為 (sa) 和保存舊的信號處理行為 (osa)struct sigaction sa, osa;// 使用 memset 將結構體清零,確保結構體中的字段沒有未初始化的值memset(&sa, 0, sizeof(sa));memset(&osa, 0, sizeof(osa));// 清空信號集,準備設置信號屏蔽sigemptyset(&sa.sa_mask);// 將 SIGINT 信號添加到屏蔽信號集中,意思是處理 SIGINT 時,其他信號會被阻塞sigaddset(&sa.sa_mask, SIGINT);// 設置自定義的信號處理函數,當 SIGINT 信號觸發時,調用 headler 函數sa.sa_handler = headler;// 注冊信號處理函數,綁定 SIGINT 信號與信號處理函數 headler,同時保存原來的信號處理方式sigaction(SIGINT, &sa, &osa);// 進入一個無限循環,模擬進程運行while (true){// 輸出當前進程的 pid,表示進程正在運行std::cout << "Process Running ...: pid: " << getpid() << std::endl;// 每 1 秒輸出一次,模擬進程持續運行sleep(1);}return 0;
}
信號處理函數 headler
:
- 該函數定義了如何處理接收到的
SIGINT
信號。當捕獲到SIGINT
信號時,會輸出信號編號,然后模擬處理過程(休眠 4秒),最后退出程序并返回狀態碼 1。
信號處理結構體 sigaction
:
sigaction
是用來定義和控制信號處理方式的結構體。通過它可以設置信號的處理函數、信號掩碼等信息。- 通過
memset
清空結構體,確保沒有未初始化的字段。 - 使用
sigemptyset
和sigaddset
配置信號屏蔽集,指定在處理SIGINT
時,阻塞其他信號(在本例中,僅阻塞SIGINT
自身)。
sigaction
調用:
sigaction(SIGINT, &sa, &osa)
設置SIGINT
信號的處理程序為headler
,并保存原來的信號處理方式(雖然在這里我們沒有使用osa
來恢復原處理方式)。
進程運行:
- 進入無限循環
while(true)
,打印當前進程的pid
(進程 ID),模擬一個長時間運行的進程。 - 每秒輸出一次進程信息,并通過
sleep(1)
使程序每秒鐘暫停一次。
信號捕獲時期的相關問題
- 信號遞歸:信號處理期間默認屏蔽當前信號,避免遞歸調用。
void headler(int signo)
{int cnt = 20;while(cnt --){std::cout << "catch signal --- signo :" << signo <<std:: endl;sleep(1);}exit(1);
}
- 在這里面
headler
方法進行循環,并且休眠一秒,在此期間持續像進發送2號信號。
- 同時可以及逆行進行多信號屏蔽。
sigaddset(&sa.sa_mask,1);
sigaddset(&sa.sa_mask,3);
sigaddset(&sa.sa_mask,4);
- 信號丟失:如果信號未及時處理,可能丟失,尤其是長時間阻塞時。
- 系統調用中斷:信號可能中斷系統調用,導致
EINTR
錯誤。 - 信號屏蔽問題:如果未正確設置信號掩碼,可能導致信號被過多屏蔽或錯誤處理。
信號pending
表的處理時機
- 信號在調用處理方法之前會被制空
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<cstring>
#include<cstdlib>// 聲明一個全局的 sigset_t 變量,用于保存掛起的信號
sigset_t pending;// 打印掛起信號的狀態(1表示掛起,0表示沒有掛起)
void PrintPending()
{sigset_t set;sigpending(&set); // 獲取當前掛起的信號集合// 遍歷信號編號,打印每個信號的狀態for (int signo = 31; signo >= 1; signo--){if (sigismember(&set, signo)) // 檢查該信號是否在掛起集合中std::cout << "1"; // 如果掛起,打印 1elsestd::cout << "0"; // 如果沒有掛起,打印 0}std::cout << "\n"; // 輸出換行
}// 信號處理程序
void headler(int signo)
{int cnt = 5; // 設置循環次數為 5while (cnt--) // 每次處理信號時,循環 5 次{PrintPending(); // 打印當前掛起的信號狀態sleep(1); // 休眠 1 秒,模擬信號處理的過程std::cout << "catch signal --- signo :" << signo << std::endl; // 輸出捕獲到的信號編號}exit(1); // 信號處理完畢后退出程序
}int main()
{signal(SIGINT, headler); // 設置 SIGINT 信號的處理函數為 headlersigset_t mask, old_mask;sigemptyset(&mask); // 初始化信號集 mask,清空所有信號sigaddset(&mask, SIGINT); // 將 SIGINT 信號加入到 mask 中,表示屏蔽 SIGINT 信號sigprocmask(SIG_BLOCK, &mask, &old_mask); // 阻塞 SIGINT 信號,并保存原信號掩碼到 old_maskint cnt = 1;while (true){sigpending(&pending); // 獲取當前掛起的信號PrintPending(); // 打印掛起信號的狀態sleep(1); // 程序每秒輸出一次狀態cnt++; // 計數器加 1std::cout << "Process Running ...: pid: " << getpid() << std::endl; // 輸出當前進程的 PIDif (cnt % 5 == 0) // 每經過 5 次循環{PrintPending(); // 再次打印掛起信號的狀態sleep(1); // 休眠 1 秒std::cout << "SIGINT UNLOCK" << std::endl; // 輸出解鎖 SIGINT 信號的提示sigprocmask(SIG_SETMASK, &old_mask, NULL); // 恢復原來的信號掩碼,解除對 SIGINT 的阻塞}}std::cout << "QUIT..." << std::endl; // 輸出退出提示return 0;
}
- 程序啟動后,會阻塞 SIGINT 信號。
- 每秒鐘,程序打印當前進程的 PID 和掛起的信號狀態。
- 每經過 5 次循環,解除對 SIGINT 信號的阻塞,允許信號被捕獲并處理。
- 捕獲到 SIGINT 信號后,
headler
會打印掛起信號狀態,處理信號并退出。