Linux系列
文章目錄
- Linux系列
- 前言
- 一、進程對信號的捕捉
- 1.1 內核對信號的捕捉
- 1.2 sigaction()函數
- 1.3 信號集的修改時機
- 二、可重入函數
- 三、volatile關鍵字
- 四、SIGCHLD信號
前言
Linux系統中,信號捕捉是指進程可以通過設置信號處理函數來響應特定信號。通過信號捕捉機制,進程可以對異步事件做出及時響應,從而提高程序的健壯性和靈活性。
一、進程對信號的捕捉
圖中內容及執行流程我已在Linux系列上上篇博客中介紹了,這里就不重復了。
1.1 內核對信號的捕捉
當信號的處理動作是用戶自定義函數,在信號遞達時就調用這個函數,這稱為捕捉信號。由于信號處理函數的代碼是在用戶空間的,處理過程比較復雜,舉例如下:
1、用戶程序注冊(對指定信號捕捉)了SIGQUIT
信號的處理函數sighandler
。
2、 當前正在執行main
函數,這時發生中斷或異常切換到內核態。
3、 在中斷處理完畢后要返回用戶態的main函數之前檢查到有信號SIGQUIT
遞達。
4、 內核決定返回用戶態后,不是恢復main函數的上下文繼續執行,而是執行sighandler
函數,sighandler
和main
函數使用不同的堆棧空間,它們之間不存在調用和被調用的關系,是兩個獨立的控制流程。
5、 sighandler
函數返回后自動執行特殊的系統調用sigreturn
再次進入內核態。
6、 再次檢測sigpending
位圖,如果沒有新的信號要遞達,這次再返回用戶態就是恢復main
函數的上下文繼續執行了。
1.2 sigaction()函數
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
功能:捕捉指定信號,并讀取和修改與指定捕捉信號相關聯的處理動作。
參數
signum:指定捕捉信號的編號。
act: 輸入型參數,act
若非空,則根據act
來修改信號的處理動作。
oldact:輸出型參數,oldact
若非空,則獲取信號原來的處理動作。
struct sigaction:系統為用戶提供的結構體類型,幫助用戶訪問內核級結構體:
今天我們主要使用,上面兩個成員對象。
- 信號處理方法,該方法需要一個整形變量,函數指針類型
act.sa_mask
所代表的是在信號處理函數執行期間需要阻塞的信號集合。也就是說,當 指定信號被捕獲并且處理函數handler
開始執行時,sa_mask
里的信號會被阻塞,一直到處理函數執行完畢。
下面我們通過兩個場景來認識他們:
例一
#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;void handler(int signum)
{cout<<"I catch a signal:"<<signum<<endl;return;
}
int main()
{struct sigaction act;struct sigaction olact;memset(&act,0,sizeof(act));//初始化內存空間memset(&olact,0,sizeof(olact));sigaction(2,&act,&olact);//對二號信號捕獲,并修改處理方法while(true){cout<<"I am process,Pid:"<<getpid()<<endl;sleep(1);}return 0;
}
可以看到這樣我們,就完成了對二號進程的捕獲并修改執行方法為自定義的行為。
例二
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<cstring>
using namespace std;void handler(int signum)
{cout<<"I catch a signal:"<<signum<<endl;sleep(10);//在執行handler方法期間,blocksig阻塞信號集中的信號被阻塞return;
}
int main()
{struct sigaction act;struct sigaction olact;memset(&act,0,sizeof(act));//初始化內存空間memset(&olact,0,sizeof(olact));sigset_t blocksig;sigaddset(&blocksig,2);//將二信號添加進blocksigact.sa_handler=handler;//將處理方法添加到act對象中act.sa_mask=blocksig;//將想要阻塞的信號位圖賦值給actsigaction(2,&act,&olact);//對二號信號捕獲,并修改處理方法while(true){cout<<"I am process,Pid:"<<getpid()<<endl;sleep(1);}return 0;
}
從執行結構可以得到,當二號信號被捕獲執行處理方法,到該方法執行結束,二號信號一直被阻塞,當解除阻塞后,二號信號再次遞達。這里也可以使用SIG_IGN
(忽略信號)、SIG_DFL
(執行默認方法),來設定act.sa_handler
。測試時建議嘗試其他信號,因為即使我們不手動的將二號信號添加到阻塞信號集,系統在執行二號信號時也會將它先阻塞,下面我們來詳細探討。
1.3 信號集的修改時機
當我們完成對指定信號的捕捉并執行對應處理方法時,操作系統會在執行該方法前,先將pending
位圖中對應信號的標志位由1
置為0
,并將該信號添加到對應的阻塞信號集中。具體來說,在二號信號處理方法執行期間,即便進程再次收到二號信號,該信號也不會被遞達。只有當上一個信號處理方法執行完畢并返回后,操作系統解除對二號信號的阻塞,新收到的二號信號才會被遞達。
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<cstring>
using namespace std;void printsig()
{sigset_t set;sigemptyset(&set);sigpending(&set);for(int i=1;i<=31;i++)//依次檢測信號集{if(sigismember(&set,i))cout<<1;else cout<<0;}cout<<endl;return ;
}
void handler(int signum)
{int cnt=5;while(cnt--){printsig();sleep(1);}cout<<"I catch a signal:"<<signum<<endl;sleep(5);//在執行handler方法期間,blocksig阻塞信號集中的信號被阻塞return;
}
int main()
{struct sigaction act;struct sigaction olact;memset(&act,0,sizeof(act));//初始化內存空間memset(&olact,0,sizeof(olact));act.sa_handler=handler;//將處理方法添加到act對象中sigaction(2,&act,&olact);//對二號信號捕獲,并修改處理方法while(true){cout<<"I am process,Pid:"<<getpid()<<endl;sleep(1);}return 0;
}
從程序執行結果可以得出,當方法被執行時,操作系統會先將pending
信號集1--->0
,并將該信號阻塞,知道上次執行結束才會完成遞達。
二、可重入函數
結合圖中展示,分析函數調用鏈
在程序運行過程中,
main
函數調用insert
函數,打算向鏈表head
中插入節點node1
。insert
函數的插入操作分為兩個步驟,當main
函數調用的insert
函數剛完成第一步時,硬件中斷出現,進程被切換到內核態。在從內核態再次返回用戶態之前,系統檢測到有信號需要處理,于是進程轉而執行sighandler
函數。在sighandler
函數中,同樣調用了insert
函數,并且向同一個鏈表head
中插入節點node2
。sighandler
函數中的insert
操作順利完成了兩個步驟,之后從sighandler
函數返回內核態,接著再次回到用戶態時,恢復上下文數據,程序從main
函數調用的insert
函數中斷處繼續執行,完成了剩余的第二步操作。原本main
函數和sig handler
函數先后嘗試向鏈表中插入兩個不同的節點,但最終鏈表中實際上僅成功插入了一個節點。
在上述執行流程中,insert函數被main和handler兩條執行流重復調用,這一情況引發了結點丟失問題,并進而導致內存泄漏。像insert函數這種在被重復調用時可能出錯或已經出錯的函數,我們稱之為不可重入函數;與之相對應的,則被稱為可重入函數。
不可重入函數的特點:
調用了malloc或free,因為malloc也是用全局鏈表來管理堆的。
調用了標準I/O庫函數。標準I/O庫的很多實現都以不可重入的方式使用全局數據結構。
三、volatile關鍵字
接下來會通過這個關鍵字,拓展部分知識
例一
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<cstring>
using namespace std;int flag=0;void handler(int signum)
{cout<<"I captured a signal:"<<signum<<endl;flag=1;
}
int main()
{struct sigaction act;memset(&act,0,sizeof(act));act.sa_handler=handler;sigaction(2,&act,nullptr);while(!flag);cout<<"process quit "<<endl;return 0;
}
mytest:mytest.ccg++ -o $@ $^ -std=c++11
相信這個執行結果大家都能理解,我就不對上面代碼作解釋了。
這里將flag設為全局變量,是因為main和sighandler是兩個獨立的執行流
例二
代碼同上
mytest:mytest.ccg++ -o $@ $^ -std=c++11 -O2
從程序執行結果可知,當將g++
編譯器的優化級別設置為-O2
時,即便通過發送二號信號(SIGINT)將flag
變量修改為1
,循環仍無法終止。這一現象的根源在于:當使用-O2
這類高級優化級別編譯代碼時,編譯器會對代碼進行多維度優化以提升執行效率。針對while(!flag);
這一循環結構,編譯器通過靜態代碼分析發現,循環體內部不存在對flag
變量的修改操作,因此推斷該變量的值在循環過程中不會發生變化。
基于“內存訪問速度相對較慢”這一特性,編譯器為減少對內存的頻繁訪問,會將flag
變量的值從內存加載至CPU寄存器中緩存。此后,在循環條件判斷時,CPU會直接從寄存器中讀取flag
的值,而非重新從內存中獲取最新數據,這就導致flag
內存不可見了。然而,信號處理機制對flag
變量的修改是直接作用于內存的,由于寄存器中的緩存值未及時刷新,導致循環條件判斷始終基于寄存器中的舊值,最終造成循環無法終止的現象。
對于上面的結果我們可以,將 flag
聲明為 volatile
類型,即 volatile int flag = 0;
。volatile
關鍵字的作用是保存flag
的內存可見性,告訴編譯器,這個變量的值可能會被意外地改變,例如被硬件或者其他線程、信號處理函數等修改,因此編譯器不能對其進行優化,這里就不展示了。
四、SIGCHLD信號
之前我們探討過使用 wait
和 waitpid
函數來清理僵尸進程。在處理子進程結束的問題上,父進程有兩種選擇:一是進行阻塞等待,直至子進程結束;二是采用非阻塞的輪詢方式,周期性地檢查是否有子進程結束,以便及時清理。然而,這兩種方式都存在明顯的弊端。若采用阻塞等待的方式,父進程在等待期間會被阻塞,無法處理自身的任務,這會極大地降低父進程的工作效率。而采用輪詢方式,雖然父進程可以在處理自身工作的同時檢查子進程的狀態,但這要求父進程時刻記得進行輪詢操作,無疑增加了程序實現的復雜度,也容易出現疏漏。實際上,當子進程終止時,它會向父進程發送 SIGCHLD
信號。該信號的默認處理方式是被忽略,但我們可以對其進行優化。父進程可以自定義 SIGCHLD
信號的處理函數,這樣一來,父進程就能夠專注于自身的工作,無需時刻關注子進程的狀態。當子進程終止時,會自動通知父進程,父進程只需在信號處理函數中調用 wait
函數,即可完成子進程的清理工作,既高效又便捷。 下面我們通過這樣的方式實現一下:
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<cstring>
#include<sys/wait.h>
#include<sys/types.h>
using namespace std;void handler(int signum)
{pid_t wid=waitpid(0,nullptr,WNOHANG);if(wid)cout<<"child quit success"<<endl;return;
}
int main()
{signal(SIGCHLD,handler);pid_t id=fork();if(id==0){sleep(5);//模擬子進程工作exit(0);}while(true){cout<<"I am father process"<<endl;sleep(1);}return 0;
}
從執行結果可以得出,子進程在退出時給父進程發送了SIGCHLD
信號。
當然還有一種防止僵尸進程的方法:父進程調 用sigaction
將SIGCHLD
的處理動作置為SIG_IGN
,這樣fork
出來的子進程在終止時會自動清理掉,不會產生僵尸進程,也不會通知父進程:
int main()
{struct sigaction act;memset(&act,0,sizeof(act));act.sa_handler=SIG_IGN;sigaction(SIGCHLD,&act,nullptr);pid_t id=fork();if(id==0){sleep(5);exit(0);}while(true){cout<<"I am father process"<<endl;sleep(1);}return 0;
}
這個結果不方便展示,你自己嘗試一下。
本篇就分享到這里了,如果文章的知識,或代碼有錯誤請您聯系我,不勝感激!!!