目錄
- 1. 進程的異常終止與core dump標志位
- 1.1 進程終止的方式
- 1.2 core方案的作用與使用方式
- 2. 信號的保存
- 2.1 信號的阻塞
- 2.2 操作系統中的sigset_t信號集類型
- 2.3 進程PCB中修改block表的系統調用接口
- 2.4 信號阻塞的相關問題驗證
1. 進程的異常終止與core dump標志位
1.1 進程終止的方式
??進程的終止方式大體可以分為兩種,正常終止與異常終止。這兩種進程終止方式的不同會表現在進程的退出碼上,進程終止退出時會返回一個int
類型的變量。此變量中只有低16bit位
會被利用起來存儲信息,具體存儲方式如下:
??當進程正常結束,進程的退出碼會寫在次低8bit位(16 ~ 8之間)
的范圍中(下標從0開始),退出碼會表示進程退出的狀態。而當進程被信號終止,異常退出時,退出碼的低7bit位(7 ~ 0之間)
的范圍內會寫入進程收到的信號。
??而信號異常終止的情況又被分為兩種,分別為Term
方案與Core
方案處理:
term
方案(termination): 直接退出,然后進程釋放其的占用的資源core
方案: 被稱之為核心轉儲,將核心信息轉移儲存到生成的Core文件
中,此文件存儲著進程異常終止的錯誤信息(發生了什么錯誤,導致錯誤發生的代碼是哪一行),便于后續排查錯誤。
??哪種信號導致的進程終止會使用哪個方案,可使用指令man 7 signal
瀏覽手冊,查看具體信息。
??退出碼的第8個bit位,則是表示此進程的退出是否為core退出,值為1表示是,值為0表示否。當core方案默認沒有打開時,core退出的信號最后的退出方式也是term,core dumped
位為0。
觀察異常退出進行的退出碼:
//打開core方案
int main()
{pid_t pid = fork();if(pid == 0){int a = 10;a /= 0;exit(0);}int status = 0;pid_t rid = waitpid(pid, &status, 0);if(rid == pid){cout << "exit code : " << ((status >> 8) & 0xFF) << endl;//退出碼 0cout << "exit signal : " << ((status) & 0x7F) << endl; //退出信號 SIGFPE : 8cout << "core dump : " << ((status >> 7) & 0x1) << endl; //core dump位 1cout << "status : " << status << endl;}return 0;
}
1.2 core方案的作用與使用方式
以下進行試驗與得出的結論都是基于Ubuntu 20.04 LTS
系統版本
core模式的打開與關閉:
??云服務器中,core模式是被默認關閉的,在想要使用core退出之前,需要先將core模式打開。當前進行的版本中,必須先執行sudo bash -c "echo core.%p > /proc/sys/kernel/core_pattern"
指令,后續才能打開core模式。
- 指令
ulimit -a
: 查看core方案是否打開
- 指令
ulimit -c file-size
: 設置core文件的大小,當core文件大小不為0時,core模式就被打開了。舊版內核中,只有root用戶才能夠執行此條命令,而新版本內核中則沒有此限制
- 指令
ulimit -c 0
: 將core文件的大小設置為0時,core模式就會被關閉
core退出后core文件的使用方式:
??當進程因為收到core
方案退出的信號而終止后,會打印出錯誤類型,錯誤信息后帶注釋信息(core dumped)
。然后會生成一個core文件
,core文件一般會有兩種,core
文件或core.pid
文件。
??當進程core方案退出后,生成了對應的core文件后,我們就可以在Linux下的gdb
調試工具中,通過core-file core/core.pid
的方式,查看core文件中的異常錯誤信息。此種core文件協助調試的方式,被稱之為事后調試。
云服務器默認與重啟后關閉core退出的原因:
??云服務器一般都是會一直運行的,以此來持續給客戶端提供各種服務。當然,在此過程中會有程序異常中斷的可能。
-
- 但因為一些服務程序的重要性,不能使其因為中斷就一直停止運行。
-
- 所以,云服務器上的重要服務異常中斷時,會使用軟件方式讓其自動重啟,以求讓其能夠正常給大部分客戶端提供服務。
-
- 可若是云服務器默認打開了core異常退出方案的設置,在不斷重啟程序的過程中,就會導致生成大量的
core.pid
文件,最后就會使得服務器的磁盤被打滿,服務器最后整個崩潰。
- 可若是云服務器默認打開了core異常退出方案的設置,在不斷重啟程序的過程中,就會導致生成大量的
??在版本的較新的內核中,會將core方案退出生成的文件都命名為core
。這樣即使服務不斷重啟,每次生成的core文件也只會不斷覆蓋,core文件始終只存在一份。服務器也就不會因此而導致崩潰。
2. 信號的保存
概念補充:
- 信號被進程接收后進行處理,信號的處理也被稱之為信號遞達
- 信號從產生到遞達之間的狀態,被稱為信號未決
2.1 信號的阻塞
1. 信號阻塞的概念:
??操作系統中,進程可以選擇對指定的信號進行阻塞,也可以稱之為屏蔽。被阻塞的信號,即使被進程接收保存,也不會被進程處理。如果一個信號被阻塞,則該信號永遠也不會被遞達,除非進程對指定信號解除阻塞。進程PCB中存在著一個變量int block
,此變量實際上是一張位圖,此位圖中的bit位都代指著一種信號,bit位的位置表示信號的編號,bit位的內容則是標識著進程對此信號是否屏蔽。若bit位的值是1,則表示進行了屏蔽,若值是0,則表示沒有進行屏蔽。
??對應信號的處理方法,在進程PCB中也有對應的一個函數指針數組handler_t* handler
做存儲與管理。再綜合之前信號產生中的知識,進程存儲信號的pending位圖。可以得知,操作系統中,進程PCB對于信號的阻塞、保存、處理分別都有一張表用于描述與管理。
2. 信號的忽略與阻塞:
??信號的阻塞與忽略并不是一種行為,對信號進行阻塞,進程是無法識別到有對應的信號到了,所以也就不會做出響應處理。而信號的忽略,則是進程已經識別到了信號,但對信號采用了忽略這一處理方式,所以,表現出的現象就是進程什么都沒有做。總而言之,阻塞就是進程沒有識別到信號,而忽略則是進程識別到了但什么都不做。
??當信號被阻塞屏蔽時,此種信號在一段時間內大量的被發送給此進程。對于普通信號pending表中只會存儲一個,在接觸阻塞之后,進程只會對最近的一次信號做處理。而對于實時信號,在遞達之前同種信號產生多次,則是會依次放在一個隊列里,此處不做詳細討論。
2.2 操作系統中的sigset_t信號集類型
??操作系統中,用戶是不能對系統的內核資源直接做修改的,只能通過操作系統提供系統調用接口來間接達成修改的目的。block信號阻塞表就屬于操作系統的內核資源,操作系統對此提供了專用的用戶級數據類型sigset_t
與系統調用接口。sigset_t
也被稱為信號集。
??此類型為操作系統提供的位圖類型,創建一個此類型變量修改其中內容并配合相應的系統調用接口就可以達到對block表的修改,即控制進程阻塞哪些信號。此外,sigset_t
類型同樣可以用于接收pending表中的內容,以此來達到讓用戶查看pending的目的。sigset_t
類型相關的系統調用接口具體如下:
- 1. 將
sigset_t
變量內容置0
#include <signal.h>
int sigemptyset(sigset_t* set);
- 2. 將所有bit位都設置為1(用于阻塞所有信號)
int sigfillset(sigset_t* set);
- 3. 將指定bit位設置為1(用于阻塞指定信號)
int sigaddset(sigset_t* set, int signo);
- 4. 將指定bit位設置為0(用于解除阻塞信號)
int sigdelset(sigset_t* set, int signo);
- 5. 用于查看信號集中是否存在某個信號(配合查看pending表)
int sigismember(const sigset_t* set, int signo);
2.3 進程PCB中修改block表的系統調用接口
#include <signal.h>//signal mask信號屏蔽字
int sigprocmask(int how, const sigset_t* set, sigset_t* oldset);
- 參數1
int how
: 系統內置宏參數,用于指定調用接口的模式
宏常量 | 作用 |
---|---|
SIG_BLOCK | 將 set 中的信號添加到當前信號屏蔽字中(阻塞這些信號) |
SIG_UNBLOCK | 將 set 中的信號從當前信號屏蔽字中移除(解除阻塞) |
SIG_SETMASK | 將當前信號屏蔽字直接設置為 set(完全覆蓋) |
- 參數2
const sigset_t* set
: block表修改的新參考信號集 - 參數3
sigset_t* oldset
: 被修改前的就信號集
??sigset_t信號集
在不同平臺下的實現方式不同,此系統內置類型不支持cout流插入或printf直接打印。sigset_t信號集
被定義與修改好后,并沒有直接達到修改block表的效果,需要配合sigprocmask接口
才能真的將數據設置進內核。
2.4 信號阻塞的相關問題驗證
- 問題1:如果一個進程將所有信號都進行屏蔽,那么這個進程是否就無法被外部終止了
??驗證方式: 創建一個死循環的進程,使用系統調用接口讓此進程屏蔽所有信號,最后讓其運行起來。待其運行之后,再使用指令對此進程嘗試一個一個發出所有信號,觀察進程反應。進程運行時,可以循環打印自己的pending表,當表bit位為1時,表示著進程收到了信號,但信號并沒有被處理,這代表信號成功被阻塞。
實驗代碼:
void PrintPending()
{sigset_t sig;sigemptyset(&sig);sigpending(&sig);for(int i = 31; i > 0; i--){if(sigismember(&sig, i)){cout << "1";}else{cout << "0";}}cout << " , pid is : " << getpid() << endl;
}int main()
{sigset_t sig;sigemptyset(&sig);for(int i = 1; i <= 31; i++)sigaddset(&sig, i);int n = sigprocmask(SIG_SETMASK, &sig, nullptr);assert(n == 0);(void)n;//如此強轉,是因為在release版本下,變量n后續若是沒有使用編譯器會報警cout << "signal mask success..." << endl;while(true){PrintPending();sleep(1);}return 0;
}
驗證結果:
??進程運行后,使用指令c=1; while [ $c -le 31 ]; do kill -$c pid; echo kill -$c; let c++; sleep 1; done
發送信號,觀察現象。
??運行程序執行指令后,可以發現,9號信號被發送后成功遞達,SIGKILL:9
號信號并不能被屏蔽。
??跳過9號信號之后繼續驗證其他信號,由運行結果發現SIGSTOP:19
號信號也沒有被屏蔽。而18號信號雖然能夠正常屏蔽,但屏蔽18號信號之后又會導致一些已經被屏蔽的信號接觸阻塞。
??操作系統如此設置的原因,是為了預防出現非法的病毒進程將自己的所有信號都設置阻塞,導致操作系統無法殺死病毒進程。
問題2:操作系統中進程在進行遞達操作時,是先將pending表中的信號bit位清0,還是先執行handler處理函數
??驗證方法: 自定義一個信號的遞達處理函數handler,讓其在handler函數查看當前進程的pending表中的對應信號bit位。若pending表中的對應信號bit位清0了,代表清0操作于handler方法之前執行,若bit位置未清0,則代表清0操作于handler方法之后。
驗證代碼:
void handler(int signo)
{sigset_t sig;sigemptyset(&sig);sigpending(&sig);if(sigismember(&sig, signo)){cout << "先執行handler" << endl;}else{cout << "先清0" << endl;}
}int main()
{signal(SIGINT, handler);while(true){cout << "process pid is : " << getpid() << endl;sleep(1);}return 0;
}
驗證結果:先清0