Linux常見訊號介紹
- 2021 年 6 月 26 日
- 筆記
1、訊號
首先訊號我們要和訊號量區分開來,雖然兩者都是作業系統進程通訊的方式。可以簡單的理解,訊號是用來通知進程發生了什麼需要做什麼,訊號量一般是用作進程同步(pv操作)
2、常見訊號量
(以下數字標號代表訊號再bitmap中的位置)
2SIGINT 可能使我們最常用的訊號之一。一般在我們想進程中斷,鍵盤輸入Ctrl + C 即可實現,這是一個進程終止訊號。
3 SIGQUIT程式異常退出訊號和2 類似, 輸入Ctrl + \ 實現
4SIGILL 執行了非法指令. 通常是因為可執行文件本身出現錯誤, 或者試圖執行數據段. 堆棧溢出時也有可能產生這個訊號。
11SIGSEGV試圖訪問未分配給自己的記憶體, 或試圖往沒有寫許可權的記憶體地址寫數據.和SIGILL一樣,程式出BUG時,我們經常見到。
17 SIGCHLD 子進程結束時, 父進程會收到這個訊號。如果父進程沒有處理這個訊號,也沒有等待(wait)子進程,子進程雖然終止,但是還會在內核進程表中佔有表項,這時的子進程稱為殭屍進程。
這種情況我們應該避免(父進程或者忽略SIGCHILD訊號,或者捕捉它,或者wait它派生的子進程,或者父進程先終止,這時子進程的終止自動由init進程來接管)。後面會詳細介紹。
19 SIGSTOP 停止(stopped)進程的執行. 注意它和terminate以及interrupt的區別:該進程還未結束, 只是暫停執行. 本訊號不能被阻塞, 處理或忽略. GDB中就使用了該訊號。
20 SIGIO 文件描述符準備就緒, 可以開始進行輸入/輸出操作.
3、訊號常見操作
我們從進程說起,在進程的控制塊中(PCB)有兩個表一個叫做訊號未決表,一個叫做訊號阻塞表(都是64點陣圖存儲)。內核首先根據訊號阻塞表中屏蔽狀態字判斷是否需要阻塞,如果該訊號被設為為了阻塞的,那麼訊號未決表中對應 狀態字(pending)相應位製成1;若該訊號阻塞解除,訊號未
決狀態字(pending)相應位製成0;表示訊號此時可以抵達了,也就是可以接收該訊號了。其實由這個地方我們可以看到,同一個時刻,如果一個訊號為阻塞了,那麼無論你到來了多少次,在解除阻塞的時候進程只會處理一次。
備註: 阻塞意味著,訊號到了,我暫時不處理,放著就是。 訊號忽略,進程看到了,但是什麼都不會做。
由此可見,對於訊號而言,要麼直接處理,要麼一會處理(也有可能一直不處理), 要麼壓根就不會處理。
我們看下系統中內置的API介面:
int sigemptyset(sigset_t *set);//將訊號集清空,共64bits
int sigfillset(sigset_t *set);//將訊號集置1
int sigaddset(sigset_t *set, int signum);//將signum對應的位置為1
int sigdelset(sigset_t *set, int signum);//將signum對應的位置為0
int sigismember(const sigset_t *set, int signum);//判斷signum是否在該訊號集合中,如果集合中該位為1,則返回1,表示位於在集合中
// 讀取更改屏蔽狀態字的API函數
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
/*
參數how有下面三種取值:
SIG_BLOCK: 將參數set指向的訊號集中設置的訊號添加到現在的屏蔽狀態字中,設置為阻塞;
SIG_UNBLOCK:將參數set指向的訊號集中設置的訊號添加到現在的屏蔽狀態字中,設置為非阻塞, 也就是解除阻塞;
SIG_SETMASK:將參數set指向的訊號集直接覆蓋現在的屏蔽狀態字的值;
如果oset是非空指針,則讀取進程的當前訊號屏蔽字通過oset參數傳出。
若成功則為0,若出錯則為-1
*/
#include <signal.h>
#include <unistd.h>
using namespace std;
void Handle(int sig) {
printf("sig = %d\n", sig);
abort();
}
void Prints(sigset_t* set) {
for (int i = 1; i <= 31; ++i) {
if (sigismember(set, i)) {
printf("1");
} else {
printf("0");
}
}
printf("\n");
}
int main(){
signal(SIGINT, Handle);
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
sigprocmask(SIG_BLOCK, &set, NULL);
// signal(SIGINT, SIG_IGN); //忽略訊號操作。
int i = 5;
while(i) {
i--;
sigset_t p;
sigpending(&p); // 獲得當前訊號未決表的數據。
Prints(&p);
sleep(2);
}
sigprocmask(SIG_UNBLOCK, &set, NULL);
//sigprocmask(SIG_UNBLOCK, &set, NULL);
//sigset_t p;
//sigpending(&p);
//Prints(&p);
printf("-1----------------------");
printf("-2----------------------");
printf("-3----------------------");
printf("-4----------------------");
return 0;
}
在上面程式碼中,signal(SIGINT, Handle),我們自己註冊了處理訊號的函數。先展示結果:
0000000000000000000000000000000
0000000000000000000000000000000
^C0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
sig = 2
簡單解釋一下,按下ctrl + c 給進程傳入了中斷的訊號,在map中位置為2的地方置為了1,因為我們阻塞了訊號,所以他不做處理。
等在循環結束以後我們解除了屏蔽,系統調用了我們註冊的函數,進行了訊號處理,在處理函數中我們調用了abort,所以剩下的程式碼並沒有執行。
如果我們把程式碼中的注釋打開再編譯運行,發現是下面的結果:
0000000000000000000000000000000
0000000000000000000000000000000
^C0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0000000000000000000000000000000
-1———————–2———————–3———————–4———————-
可以看到,SIGINT訊號,到的時候,不是先忽略,是先判定阻塞,放在了未決訊號表中。
然後等訊號解阻塞以後,可以看到,進程忽略了該訊號。不做處理。
4、SIGCHLD訊號
這個訊號,我們上面解釋說明過。子進程退出時,會給父進程發送這個命令,讓父進程回收資源。如果父進程不做處理,那麼就成了子進程就成了我們說的殭屍進程,造成記憶體泄漏(敲黑板,這玩意也會造成記憶體泄漏)。
如果子進程退出的時候,父進程早就沒了,那麼回收資源的工作交給init進程。一般情況下,如果父進程,不關心子進程的資源回收,也不期待從兒子那邊獲得什麼,可以對這個訊號進行忽略,像上面程式碼中那樣操作即可
signal(SIGCHLD, SIG_IGN)
。這樣也不會產生殭屍進程,子進程退出的時候,發個訊號給父進程。父進程處理方式是忽略,子進程就自己退出了。
當然也可以自己創建處理函數,處理該訊號。
在引入程式碼之前我們先說兩個函數:wait 和 waitpid
頭文件sys/wait.h
pid_t wait(int *status);
進程一旦調用了wait,就立即阻塞自己,由wait自動分析是否當前進程的某個子進程已經退出,如果讓它找到了這樣一個已經變成殭屍的子進程,wait就會收集這個子進程的資訊,並把它徹底銷毀後返回;如果沒有找到這樣一個子進程,wait就會一直阻塞在這裡,直到有一個出現為止。
參數status用來保存被收集進程退出時的一些狀態,它是一個指向int類型的指針。但如果我們對這個子進程是如何死掉的毫不在意,只想把這個殭屍進程消滅掉,(事實上絕大多數情況下,我們都會這樣想),我們就可以設定這個參數為NULL,就象下面這樣: pid = wait(NULL);
pid_t waitpid(pid_t pid, int *status, int options);
參數:
status:如果不是空,會把狀態資訊寫到它指向的位置,與wait一樣
options:允許改變waitpid的行為,最有用的一個選項是WNOHANG,它的作用是防止waitpid把調用者的執行掛起
The value of options is an OR of zero or more of the following con-
stants:
WNOHANG return immediately if no child has exited.
wait與waitpid區別:
在一個子進程終止前, wait 使其調用者阻塞,而waitpid 有一選擇項,可使調用者不阻塞。
waitpid並不等待第一個終止的子進程—它有若干個選擇項,可以控制它所等待的特定進程。
實際上wait函數是waitpid函數的一個特例。waitpid(-1, &status, 0);
了解這些以後我們看程式碼例子:
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
void waitchildren(int signo) {
int status;
wait(&status);
//while(1)waitpid(-1, &status, WNOHANG); //注釋3
//while(1)wait(&status) //注釋4
}
int main() {
int i;
pid_t pid;
//signal(SIGCHLD, waitchildren); //注釋1
//signal(SIGCHLD, SIG_IGN); //注釋2
for(i=0; i<100; i++) {
pid = fork();
if(pid == 0)
break;
}
if(pid>0) {
printf("press Enter to exit...");
getchar();
}
return 0;
}
如果 我們編譯運行,發現在不退出的情況下,重新開一個終端運行top命令,可以看到zombie欄位為100.即是100個殭屍進程。
如果打開注釋1 發現還是有殭屍進程,但是數量不為100,同一時刻,很多子進程退出,但是可惜只處理一部分。
關閉注釋1打開注釋2.發現一個殭屍進程都沒了。
如果只打開注釋3 你也會發現一個殭屍進程都沒了。
如果你只打開注釋4,你會發現程式只處理了一個,而且卡死了。
wait 我們可以稱其為同步介面,而waitpid為非同步介面。
再看下面這段程式碼:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <iostream>
int count = 0;
void waitchildren(int signo) {
count++;
std::cout << count << "------------" << std::endl;
int status;
wait(&status);
}
int main() {
int i;
pid_t pid;
signal(SIGCHLD, waitchildren);
//signal(SIGCHLD, SIG_IGN);
sigset_t set;
sigaddset(&set, SIGCHLD);
sigprocmask(SIG_BLOCK, &set, NULL);
for(i=0; i<100; i++) {
pid = fork();
if(pid == 0)
break;
}
if(pid>0) {
printf("press Enter to exit...");
getchar();
sigprocmask(SIG_UNBLOCK, &set, NULL);
}
return 0;
}
這段程式你會發現,只有一個子進程發送的訊號,被捕獲處理了。
以上就是全部內容,歡迎各位大佬,糾錯指正。