一、信號發送
- 信號動作
通過指令man -7 signal查看信號的手冊,可以找到普通信號發出后對應的操作、信號編號和詳細描述信息。
- 信號發送的本質
普通信號的發送本質上是將信號寫入進程的PCB結構體中的位圖(pending位圖)。這個位圖對應著1到31號的普通信號,收到信號后將對應比特位置為1,表示信號已收到,然后PCB執行相應的工作。值得注意的是,如果連續發送普通信號,進程只會處理最后一次的信號,因為每次寫入都是覆蓋式的。
實時信號與普通信號類似,但它們使用的是結構體而非位圖,信號被組織在隊列中,遵循先入先出的規則。如果連續發送實時信號,進程會依次處理隊列中的所有信號。
- Core Dump
在《進程控制》一文中,我們解釋了wait函數的status參數,其中第7位是core dump標志。程序在運行過程中發生崩潰(如段錯誤、除零錯誤等)時,Core dump會記錄程序崩潰瞬間的內存狀態,包括寄存器的值、調用棧信息、全局變量和局部變量的值等。開發人員可以使用調試工具(如GDB)加載Core dump文件,分析這些信息以找出程序崩潰的原因。
可以通過ulimit -c 10240將core文件的大小限制修改為10240字節。云服務器上通常默認core文件大小限制為0,出現錯誤時可能導致core文件迅速填滿。我們可以根據需要修改其大小限制。生成的文件名為core.pid,其中pid是出錯進程的pid。例如,假設test進程發生錯誤,其pid為12314,我們可以在GDB模式下輸入gdb test core.12314來打印錯誤信息和原因。
二、信號的保存
- 前置概念
實際執行信號處理動作稱為信號遞達。信號從產生到遞達之間的狀態稱為信號未決。被阻塞的信號會保持在未決狀態,直到進程解除對此信號的阻塞,才會執行遞達的動作。
- 阻塞信號
信號被阻塞時,會進入信號未決狀態。阻塞功能通過一個31位的位圖block實現,對應1到31號信號。當對應比特位為1時,表示該信號被阻塞,為0則表示不阻塞。如果信號被阻塞,則進入阻塞態;如果沒有被阻塞,則進入未決狀態。
- 保存信號
未決狀態的作用是保存信號,通過位圖pending實現。pending與block一致,對應的下標和信號編號一一對應。當對應比特位為1時,表示該信號處于未決狀態,為0則表示信號已遞達。實際上,block和pending都用于保存信號,但由于有兩個位圖,我們分開討論。
- 信號遞達
信號遞達后,會執行相應的行為,包括SIG_DFL(默認處理動作)、SIG_IGN(忽略)和自定義處理sighandler。
- 總結
一個信號首先經過block,如果block為0,則來到pending,如果pending為0,則來到handler執行動作。9號和19號信號是特例,它們不能被阻塞和保存,一旦發出直接執行handler,對應的行為只能是默認動作終止和暫停。
三、信號集操作函數
信號集操作函數用于操作信號集,sigset_t是操作系統提供的數據類型,用于描述位圖。以下是信號集操作函數:
#include <signal.h> int sigemptyset(sigset_t *set); // 將位圖全部設置為0 int sigfillset(sigset_t *set); // 將位圖全部設置為1 int sigaddset(sigset_t *set, int signo); // 將位圖中的某一位設置為1 int sigdelset(sigset_t *set, int signo); // 將位圖中的某一位設置為0 int sigismember(const sigset_t *set, int signo); // 判斷一個信號是否在信號集中,不在返回0,在返回1,出錯返回-1 </signal.h>
- 設置block位圖
sigprocmask是一個重要的系統調用,用于檢查和修改進程的信號掩碼(阻塞信號集)。
#include <signal.h> int sigprocmask(int how, const sigset_t *set, sigset_t *oset); </signal.h>
返回值:成功返回0,失敗返回-1。
how參數指定對信號掩碼的操作方式:
how取值 | 含義 | 示例說明 |
---|---|---|
SIG_BLOCK | 將set所指向的信號集中的信號添加到當前的信號掩碼中,即阻塞set中的信號 | 若當前信號掩碼已阻塞SIGINT,使用SIG_BLOCK并傳入包含SIGTERM的信號集,SIGTERM也會被阻塞 |
SIG_UNBLOCK | 從當前的信號掩碼中移除set所指向的信號集中的信號,即解除對set中信號的阻塞 | 若當前信號掩碼阻塞了SIGINT和SIGTERM,使用SIG_UNBLOCK并傳入包含SIGINT的信號集,SIGINT信號的阻塞狀態將被解除 |
SIG_SETMASK | 將當前的信號掩碼設置為set所指向的信號集,覆蓋原來的信號掩碼 | 若原信號掩碼阻塞SIGINT,使用SIG_SETMASK并傳入包含SIGTERM的信號集,信號掩碼將只阻塞SIGTERM |
set參數指向一個sigset_t類型的信號集,包含要操作的信號。如果how的值為SIG_BLOCK或SIG_UNBLOCK,set表示要添加或移除的信號集;如果how的值為SIG_SETMASK,set表示要設置的新信號掩碼。如果該參數為NULL,則不改變當前的信號掩碼,僅獲取當前信號掩碼,此時oset不能為NULL。
oset參數指向一個sigset_t類型的信號集,用于存儲調用sigprocmask之前的信號掩碼。如果不需要保存舊的信號掩碼,可以將該參數設置為NULL。
- 設置pending位圖
sigpending用于獲取進程當前未決信號集。
#include <signal.h> int sigpending(sigset_t *set); </signal.h>
返回值:成功返回0,失敗返回-1。
set參數用于存儲當前進程中處于未決狀態(即已發送但由于被阻塞而尚未被處理)的信號集。
- 設置handler行為
信號處理行為有三種情況:默認、忽略和自定義。自定義處理可以通過signal函數實現,此前已有介紹,不再贅述。
四、驗證信號保存行為
#include <iostream> #include <signal.h> #include <unistd.h> using namespace std; // 打印出位圖 void PrintPending(const sigset_t &pset) { for(int i = 31; i >= 1; i--) { cout << (sigismember(&pset, i) ? 1 : 0); } cout << endl; } // 信號處理函數 void handler(int sig) { cout << "Caught signal " << sig << endl; } int main() { sigset_t newmask, oldmask, pendmask; // 初始化信號集 sigemptyset(&newmask); sigaddset(&newmask, SIGINT); // 阻塞SIGINT信號 sigprocmask(SIG_BLOCK, &newmask, &oldmask); // 打印當前未決信號集 while(1) { sigpending(&pendmask); PrintPending(pendmask); sleep(1); } // 解除SIGINT信號的阻塞 sigprocmask(SIG_SETMASK, &oldmask, NULL); return 0; } </unistd.h></signal.h></iostream>
在3秒后按下ctrl+c,捕捉信號函數不會工作,說明信號被阻塞了。15秒后自動解除阻塞,立即打印出handler函數定義要打印的信息,再按ctrl+c就正常執行handler行為。