Linux常見訊號介紹

  • 2021 年 6 月 26 日
  • 筆記

1、訊號

首先訊號我們要和訊號量區分開來,雖然兩者都是作業系統進程通訊的方式。可以簡單的理解,訊號是用來通知進程發生了什麼需要做什麼,訊號量一般是用作進程同步(pv操作)

2、常見訊號量

(以下數字標號代表訊號再bitmap中的位置)

SIGINT 可能使我們最常用的訊號之一。一般在我們想進程中斷,鍵盤輸入Ctrl + C 即可實現,這是一個進程終止訊號。

SIGQUIT程式異常退出訊號和2 類似, 輸入Ctrl + \ 實現

SIGILL 執行了非法指令. 通常是因為可執行文件本身出現錯誤, 或者試圖執行數據段. 堆棧溢出時也有可能產生這個訊號。

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;  
}  

這段程式你會發現,只有一個子進程發送的訊號,被捕獲處理了。

以上就是全部內容,歡迎各位大佬,糾錯指正。