Linux信號機(jī)制之信號的保存與處理技巧分享
前言:在Linux操作系統(tǒng)的廣闊天地中,信號機(jī)制無疑是一個充滿挑戰(zhàn)與機(jī)遇的領(lǐng)域。信號,作為進(jìn)程間通信的一種重要方式,不僅承載著豐富的信息,還扮演著進(jìn)程控制與管理的重要角色。然而,對于許多初學(xué)者而言,信號的保存與處理往往是一個難以逾越的障礙
讓我們一同踏上這段充滿探索與發(fā)現(xiàn)的旅程,共同揭開Linux信號機(jī)制的神秘面紗吧!
1. 信號的保存
信號其他相關(guān)常見概念
- 實(shí)際執(zhí)行信號的處理動作稱為信號遞達(dá)(Delivery)
- 信號從產(chǎn)生到遞達(dá)之間的狀態(tài),稱為信號未決(Pending)
- 進(jìn)程可以選擇阻塞 (Block )某個信號
- 被阻塞的信號產(chǎn)生時將保持在未決狀態(tài),直到進(jìn)程解除對此信號的阻塞,才執(zhí)行遞達(dá)的動作
- 注意:阻塞和忽略是不同的,只要信號被阻塞就不會遞達(dá),而忽略是在遞達(dá)之后可選的一種處理動作
在內(nèi)核中的表示
在Linux內(nèi)核中,信號的保存主要依賴于三種數(shù)據(jù)結(jié)構(gòu):pending表、block表和handler表
pending表:
- pending表是一張位圖(bitmap),用于記錄當(dāng)前進(jìn)程是否收到了信號,以及收到了哪些信號
- 當(dāng)進(jìn)程接收到一個信號時,對應(yīng)的信號位圖上的比特位就會由0置1,表示該信號處于未決(Pending)狀態(tài)
block表:
- block表也是一張位圖,用于記錄特定信號是否被屏蔽(阻塞)
- 比特位的內(nèi)容為0表示不屏蔽,為1表示屏蔽。屏蔽的信號在解除屏蔽之前不會被操作系統(tǒng)處理
handler表:
- handler表是一個函數(shù)指針數(shù)組,用于保存每個信號對應(yīng)的處理方法
- 這些處理方法可以是默認(rèn)的,或者忽略的,當(dāng)然也可以是用戶自定義的。當(dāng)信號被遞達(dá)時,操作系統(tǒng)會根據(jù)handler表找到對應(yīng)的處理方法并執(zhí)行

舉個例子:上圖SIGINT信號產(chǎn)生過,但正在被阻塞,所以暫時不能遞達(dá)。雖然它的處理動作是忽略,但在沒有解除阻塞之前不能忽略這個信號,因?yàn)檫M(jìn)程仍有機(jī)會改變處理動作之后再解除阻塞
sigset_t
sigset_t是一個在Unix和Linux系統(tǒng)中用于表示信號集的數(shù)據(jù)類型。信號集本質(zhì)上是一個信號的集合,用于指定多個信號,通過使用sigset_t,可以輕松地指定一組信號,并在諸如信號阻塞、信號等待等操作中使用這組信號
sigset_t信號集操作函數(shù):
- sigemptyset():初始化信號集,將其設(shè)置為空集
- sigfillset():初始化信號集,將其設(shè)置為包含所有信號的集合
- sigaddset():向信號集中添加一個信號
- sigdelset():從信號集中刪除一個信號
- sigismember():檢查一個信號是否屬于某個信號集
2. 信號集操作函數(shù)
信號集操作函數(shù)用于處理與信號集(sigset_t類型)相關(guān)的操作。這些函數(shù)允許用戶初始化信號集、添加或刪除信號、檢查信號是否存在于信號集中,以及修改進(jìn)程的信號屏蔽字
sigprocmask()函數(shù):
讀取或更改進(jìn)程的信號屏蔽字(阻塞信號集)

返回值:若成功則為0,若出錯則為-1
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
如果oset是非空指針,則讀取進(jìn)程的當(dāng)前信號屏蔽字通過oset參數(shù)傳出。如果set是非空指針,則 更改進(jìn)程的信號屏蔽字,參數(shù)how指示如何更改。如果oset和set都是非空指針,則先將原來的信號 屏蔽字備份到oset里,然后根據(jù)set和how參數(shù)更改信號屏蔽字。假設(shè)當(dāng)前的信號屏蔽字為mask,下表說明了how參數(shù)的可選值

代碼示例:
void headler(int signo)
{
cout << "headler: " << signo << endl;
// exit(0);
}
int main()
{
cout << "pid: " << getpid() << endl;
signal(2, headler);
sigset_t block, oblock;
// 初始化
sigemptyset(&block);
sigemptyset(&oblock);
sigaddset(&block, 2); // 設(shè)置對2號信號的屏蔽
sigprocmask(SIG_BLOCK, &block, &oblock);
while(1)
{
sleep(1);
}
return 0;
}

那我們到底能不能屏蔽所有普通信號呢?我們來測試一下
修改代碼:
for(int signo = 1; signo <= 31; signo++) sigaddset(&block, signo);


我們發(fā)現(xiàn)9號信號,19號信號是不會被屏蔽的
注意:如果調(diào)用sigprocmask解除了對當(dāng)前若干個未決信號的阻塞,則在sigprocmask返回前,至少將其中一個信號遞達(dá)
sigpending()函數(shù):
讀取當(dāng)前進(jìn)程的未決信號集,通過set參數(shù)傳出

返回值:調(diào)用成功則返回0,出錯則返回-1
int sigpending(sigset_t *set);
代碼示例:
void PrintPending(const sigset_t &pending)
{
for(int signo = 32; signo > 0; signo--)
{
if(sigismember(&pending, signo))
{
cout << "1";
}
else{
cout << "0";
}
}
cout << endl;
}
int main()
{
cout << "pid: " << getpid() << endl;
// 屏蔽2號信號
sigset_t set, oset;
sigemptyset(&set);
sigemptyset(&oset);
sigaddset(&set, 2);
sigprocmask(SIG_BLOCK, &set, &oset);
int cnt = 0;
// 讓進(jìn)程不斷獲取當(dāng)前進(jìn)程的pending
sigset_t pending;
while(1)
{
sigpending(&pending);
PrintPending(pending);
sleep(1);
// 對2好信號進(jìn)行解除屏蔽
cnt++;
if(cnt == 16)
{
cout << "對2號信號進(jìn)行解除屏蔽,準(zhǔn)備遞達(dá)" << endl;
sigprocmask(SIG_SETMASK, &oset, nullptr);
}
}
return 0;
}

當(dāng)我們對信號進(jìn)行處理的時候,會先將pending位圖中的1 -> 0,然后再去調(diào)用信號捕捉方法
3. 信號的處理
進(jìn)程從內(nèi)核態(tài)返回到用戶態(tài)的時候(包含身份的變化),進(jìn)行信號的檢測和信號的處理
- 用戶態(tài)是一種受控的狀態(tài),能夠訪問的資源是有限的(只能訪問自己的[ 0 - 3GB] )
- 內(nèi)核態(tài)是一種操作系統(tǒng)的工作狀態(tài),能夠訪問大部分系統(tǒng)資源(可以讓用戶以O(shè)S的身份訪問[ 3 - 4GB])
調(diào)用系統(tǒng)調(diào)用接口就是在進(jìn)程地址空間中進(jìn)行的!



sigaction
sigaction是一個POSIX標(biāo)準(zhǔn)的系統(tǒng)調(diào)用,用于更改和檢查信號的處理方式。與傳統(tǒng)的signal函數(shù)相比,sigaction提供了更多的控制選項和更可靠的信號處理方式

int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);
- signum:信號編號,指定要設(shè)置的信號
- act:指向sigaction結(jié)構(gòu)的指針,在sigaction的實(shí)例中指定了對特定信號的處理。如果為NULL,則進(jìn)程會以缺省方式對信號處理
- oldact:指向的對象用來保存原來對相應(yīng)信號的處理,如果為NULL,則不保存
act和oldact指向sigaction結(jié)構(gòu)體

代碼示例:
void Print(const sigset_t &pending);
void handler(int signo)
{
cout << "get a signo: " << signo << endl;
while(1)
{
sigset_t pending;
sigpending(&pending);
Print(pending);
sleep(1);
}
}
void Print(const sigset_t &pending)
{
for(int signo = 31; signo > 0; signo--)
{
if(sigismember(&pending, signo))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
int main()
{
cout << "pid: " << getpid() << endl;
struct sigaction act, oact;
act.sa_handler = handler;
// 增加對3號信息的屏蔽
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, 3);
// 對2信號進(jìn)行屏蔽
sigaction(2, &act, &oact);
while(1) sleep(1);
return 0;
}

當(dāng)某個信號的處理函數(shù)被調(diào)用時,內(nèi)核自動將當(dāng)前信號加入進(jìn)程的信號屏蔽字,當(dāng)信號處理函數(shù)返回時自動恢復(fù)原來的信號屏蔽字,這樣就保證了在處理某個信號時,如果這種信號再次產(chǎn)生,那么 它會被阻塞到當(dāng)前處理結(jié)束為止,如果在調(diào)用信號處理函數(shù)時,除了當(dāng)前信號被自動屏蔽之外,還希望自動屏蔽另外一些信號,則用sa_mask字段說明這些需要額外屏蔽的信號,當(dāng)信號處理函數(shù)返回時自動恢復(fù)原來的信號屏蔽字
多個信號情況:
代碼示例:
void Print(const sigset_t &pending);
void handler(int signo)
{
cout << "get a signo: " << signo << endl;
sleep(1);
}
void Print(const sigset_t &pending)
{
for(int signo = 31; signo > 0; signo--)
{
if(sigismember(&pending, signo))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
int main()
{
signal(2, handler);
signal(3, handler);
signal(4, handler);
signal(5, handler);
sigset_t mask, omask;
sigemptyset(&mask);
sigemptyset(&omask);
sigaddset(&mask, 2);
sigaddset(&mask, 3);
sigaddset(&mask, 4);
sigaddset(&mask, 5);
sigprocmask(SIG_SETMASK, &mask, &omask);
cout << "pid: " << getpid() << endl;
int cnt = 20;
while(1)
{
sigset_t pending;
sigpending(&pending);
Print(pending);
cnt--;
sleep(1);
if(cnt == 0)
{
sigprocmask(SIG_SETMASK, &omask, nullptr);
cout << "cancel 2,3,4,5 block" << endl;
}
}
return 0;
}

由實(shí)驗(yàn)結(jié)果來看,我們系統(tǒng)是等所有的信號處理完全了,統(tǒng)一再進(jìn)行返回的,并且他并不是按照順序來處理信號的

4. 可重入函數(shù)
可重入函數(shù)是指可以被多個任務(wù)(如線程、進(jìn)程)同時調(diào)用,并且能保證每個任務(wù)調(diào)用該函數(shù)時都能得到正確結(jié)果的函數(shù)。換句話說,這種函數(shù)在執(zhí)行的任何時刻都可以被中斷,然后在中斷點(diǎn)恢復(fù)執(zhí)行而不會導(dǎo)致錯誤

- main函數(shù)調(diào)用 insert函數(shù)向一個鏈表head中插入節(jié)點(diǎn)node1,插入操作分為兩步,剛做完第一步的 時候,因?yàn)橛布袛嗍惯M(jìn)程切換到內(nèi)核,再次回用戶態(tài)之前檢查到有信號待處理,于是切換 到sighandler函數(shù),sighandler也調(diào)用insert函數(shù)向同一個鏈表head中插入節(jié)點(diǎn)node2,插入操作的 兩步都做完之后從sighandler返回內(nèi)核態(tài),再次回到用戶態(tài)就從main函數(shù)調(diào)用的insert函數(shù)中繼續(xù) 往下執(zhí)行,先前做第一步之后被打斷,現(xiàn)在繼續(xù)做完第二步。結(jié)果是,main函數(shù)和sighandler先后 向鏈表中插入兩個節(jié)點(diǎn),而最后只有一個節(jié)點(diǎn)真正插入鏈表中
- insert函數(shù)被不同的控制流程調(diào)用,有可能在第一次調(diào)用還沒返回時就再次進(jìn)入該函數(shù),這稱為重入,insert函數(shù)訪問一個全局鏈表,有可能因?yàn)橹厝攵斐慑e亂,像這樣的函數(shù)稱為 不可重入函數(shù),反之,如果一個函數(shù)只訪問自己的局部變量或參數(shù),則稱為可重入(Reentrant) 函數(shù)
不可重入函數(shù)(符合以下任一條件):
- 調(diào)用了malloc或free,因?yàn)閙alloc也是用全局鏈表來管理堆的
- 調(diào)用了標(biāo)準(zhǔn)I/O庫函數(shù),標(biāo)準(zhǔn)I/O庫的很多實(shí)現(xiàn)都以不可重入的方式使用全局?jǐn)?shù)據(jù)結(jié)構(gòu)
5. volatile
volatile是一個類型修飾符,用于告訴虛擬機(jī)該變量是極有可能多變的,從而免于一些優(yōu)化措施,確保變量的正確性和線程間的通信。它主要用于多線程環(huán)境下的變量共享,確保變量的可見性和有序性
代碼示例:
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
int flag = 0;
void headler(int signo)
{
cout << "signo: " << signo << endl;
flag = 1;
cout << "change flag to: " << flag << endl;
}
int main()
{
signal(2, headler);
cout << "pid: " << getpid() << endl;
while(!flag);
cout << "qiut normal!" << endl;
return 0;
}
標(biāo)準(zhǔn)情況下,鍵入 CTRL-C ,2號信號被捕捉,執(zhí)行自定義動作,修改 flag=1 , while 條件不滿足,退出循環(huán),進(jìn)程退出

優(yōu)化情況下(-O2)(不是數(shù)字0),鍵入 CTRL-C ,2號信號被捕捉,執(zhí)行自定義動作,修改 flag=1 ,但是 while 條件依舊滿足,進(jìn)程繼續(xù)運(yùn)行


所以要想不讓編譯器優(yōu)化,我們需要加上volatile
volatile int flag = 0;

6. 總結(jié)
SIGCHLD信號(了解)
SIGCHLD信號在子進(jìn)程狀態(tài)改變時發(fā)送給其父進(jìn)程。子進(jìn)程的狀態(tài)改變包括以下幾種情況:
- 子進(jìn)程終止,無論是正常終止還是異常終止(如有core dump或無core dump)
- 子進(jìn)程停止,例如接收到SIGSTOP信號
- 停止的子進(jìn)程被SIGCONT信號喚醒并繼續(xù)執(zhí)行
代碼示例:
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
void handle(int signo) {
int status;
pid_t pid;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
if (WIFEXITED(status)) {
printf("Child %d exited with status %d\n", pid, WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("Child %d killed by signal %d\n", pid, WTERMSIG(status));
}
}
}
int main() {
pid_t pid;
struct sigaction act;
// 設(shè)置SIGCHLD信號的處理函數(shù)
act.sa_handler = handle;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGCHLD, &act, NULL);
// 創(chuàng)建子進(jìn)程
pid = fork();
if (pid < 0)
{
perror("fork");
exit(1);
}
else if (pid == 0)
{
// 子進(jìn)程代碼
printf("Child process (PID: %d) is running\n", getpid());
sleep(5); // 模擬子進(jìn)程工作
exit(0); // 子進(jìn)程正常退出
}
else
{
// 父進(jìn)程代碼
printf("Parent process (PID: %d) is running\n", getpid());
// 父進(jìn)程可以繼續(xù)執(zhí)行其他任務(wù),等待SIGCHLD信號來回收子進(jìn)程
while (1) {
sleep(10); // 模擬父進(jìn)程工作
printf("Parent process is still running\n");
}
}
return 0;
}
父進(jìn)程設(shè)置了SIGCHLD信號的處理函數(shù)handle_sigchld,該函數(shù)會在子進(jìn)程狀態(tài)改變時被調(diào)用。在處理函數(shù)中,父進(jìn)程使用waitpid()函數(shù)來回收子進(jìn)程的資源

隨著我們對Linux中信號保存與處理機(jī)制的深入探討,我們不難發(fā)現(xiàn),信號不僅是進(jìn)程間通信的一種重要手段,更是Linux操作系統(tǒng)內(nèi)核提供的一種強(qiáng)大而靈活的控制機(jī)制。通過信號的捕獲、保存、處理以及恢復(fù),我們可以實(shí)現(xiàn)對進(jìn)程行為的精確控制,從而滿足各種復(fù)雜的系統(tǒng)需求
以上就是Linux信號機(jī)制之信號的保存與處理技巧分享的詳細(xì)內(nèi)容,更多關(guān)于Linux信號機(jī)制的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Windows10安裝linux子系統(tǒng)的兩種方式(圖文詳解)
這篇文章主要介紹了Windows10安裝linux子系統(tǒng)的兩種方式,文中通過圖文介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06
Linux系統(tǒng)下Nginx支持ipv6配置的方法
這篇文章主要介紹了Linux系統(tǒng)下Nginx支持ipv6的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-12-12
PHP程序員玩轉(zhuǎn)Linux系列 Linux和Windows安裝nginx
這篇文章主要為大家詳細(xì)介紹了PHP程序員玩轉(zhuǎn)Linux系列文章,Linux和Windows安裝nginx教程,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-04-04
winxp apache用php建本地虛擬主機(jī)的方法
windows xp用php建本地虛擬主機(jī)的方法(注:以下目錄是筆者系統(tǒng)目錄)2009-07-07

