Linux進程間通訊(二)

訊號

訊號的概念

訊號是Linux進程間通訊的最古老的一種方式。訊號是軟體中斷,是一種非同步通訊的方式。訊號可以導致一個正在運行的進程被另一個正在運行的非同步進程中斷,轉而處理某個突發事件。

一旦產生訊號,就要執行訊號處理函數,處理完訊號處理函數,再回來執行主函數,這就是中斷。

一個完整的訊號周期包括三個部分:訊號的產生,訊號在進程中的註冊,訊號的進程中的銷毀,執行訊號處理函數。

注意:這裡訊號的產生、註冊、註銷都是訊號的內部機制,而不是訊號的函數實現。

查看訊號

可以通過kill -l命令查看系統定義的訊號列表:

可以看到,不存在編號為0的訊號。其中1-31號訊號成為常規訊號(也叫普通訊號和標準訊號),34-64稱為實時訊號,驅動編程與硬體相關。名字上差別不大,而前32個名字各不相同。

如果你想了解某個訊號的產生條件和默認處理動作,可以通過指令man signal_id signal

例如: man 2 signal 就是查看二號訊號的作用

訊號四要素

  • 編號
  • 名稱
  • 事件
  • 默認處理動作

可以看到有一些訊號具有三個「value」,第一個值通常對alpha和sparc架構有用,中間值針對x86和arm架構,最後一個應用於mips架構。

不同的作業系統定義了不同的系統訊號,這裡我們之研究linux系統中的訊號。

Action為默認動作:

  • Term:終止進程
  • Ign:忽略訊號
  • Core:終止進程,生成Core文件。(檢查死亡原因,用於gdb調試)
  • Stop:終止(暫停)進程
  • Cont:繼續執行進程
  • 強調 9)SIGKILL和19)SIGSTOP訊號,不允許忽略和捕捉,只能執行默認的動作,甚至不能將其設為阻塞

訊號的產生

通過按鍵產生

當用戶按下某些終端按鍵的時候,將產生訊號

  • 按鍵Ctrl C可以發送2號訊號(SIG_INT),默認處理動作是終止進程
  • 按鍵按下Ctrl \,發送3號訊號(SIG_QUIT),默認處理動作是終止進程並且Core Dump
  • 按鍵按下Ctrl z查安生終端訊號SIGSTOP ,默認動作是暫停進程的執行

Core Dump是什麼?

當一個進程要異常終止時,可以選擇把進程的用戶空間記憶體數據全部保存到磁碟上,文件名通常是core,這叫做Core Dump。我們可以通過使用gdb調試查看core文件查看進程退出的原因,這也叫事後調試

通過系統調用

下面介紹三個系統函數

(1)kill函數

#include<sys/types.h>
#include<signal.h>
int kill(pid_t pid,int sig);
功能:給指定進程發送指定訊號(不一定殺死)
參數:
    pid:取值有4種情況:
      pid > 0:將訊號傳送給進程ID為pid的進程。
      pid = 0:將訊號傳送給當前進程所在進程組中的所有進程。
      pid = -1:將訊號傳送給系統內所有的進程。
      pid < -1:將訊號傳給指定進程組的所有進程。這個進程組號等於pid的絕對值。
      sig:訊號的編號,這裡可以填數字編號,也可以填訊號的宏定義,可以通過
命令kill -l進行相應查看。不準薦直接使數字,應使用宏名,因為不同作業系統訊號編號可能不同,但名稱一致。
返回值.
    成功:0
    失敗:-1
  • super用戶(root)可以發送訊號給任意用戶,普通用戶是不能向系統用戶發送訊號的。
  • 普通用戶不能向其他普通用戶發送訊號,終止其進程,只能向自己創建的進程發送訊號。

程式碼示例:

#include<stdio.h>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>
 int main()
 {  
    pid_t pid;
    //創建一個子進程
    pid = fork();
     {  
        printf("child process do work.....\n");
        sleep(1);
     }
      exit(0);//子進程退出
    }
    else
    {  
      //父進程
      sleep(3);
      printf("子進程不聽話了,該退出了....\n");
      kill(pid,15);
      printf("父進程該結束了,已經完成了它的使命\n");
  
    }
   return 0;
 }

運行結果如下

(2)raise函數

#include<signal.h>
int raise(int sig);
功能:給當前進程發送指定訊號(自己給自己發),等價於kill(getpid(),sig)
參數:
	sig:訊號編號
返回值:成功:0
       失敗:非0值

程式碼示例:

#include<stdio.h>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>
 int main()
 {  
   int i = 1;
   while(1)
   {
   		printf("do working %d\n",i);
   		//給自己發送一個訊號
   		if(i == 4)
   		{
   			//自己給自己發送編號為15的訊號
   			raise(SIGTERM);
   		}
   		i ++;
   		sleep(1);
   }
   return 0;
 }

運行結果如下:

(3)abort函數

#include<stdlib.h>
void abort(void);
功能:給自己發送異常終止訊號6)SIGABRT,並且產生core文件,等價於kill(getpid(),SIGABRT);
參數:無
返回值:無

程式碼示例:

#include<stdio.h>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>
 int main()
 {  
   int i = 1;
   while(1)
   {
   		printf("do working %d\n",i);
   		//給自己發送一個訊號
   		if(i == 4)
   		{
   			//給自己發送一個編號為6的訊號,默認的行為就是終止進程
   			abort();
   		}
   		i ++;
   		sleep(1);
   }
   return 0;
 }

運行結果如下:

通過軟體條件產生

在上一篇部落格介紹過,管道如果讀端不讀了,存儲系統會發生SIGPIPE 訊號給寫端進程,終止進程。這個訊號就是由一種軟體條件產生的,這裡再介紹一種由軟體條件產生的訊號SIGALRM(時鐘訊號)。

#include<unistd.h>
unsigned int alarm(unsigned int seconds);
功能:
    設置定時器(鬧鐘)。在指定seconds後,內核會給當前進程發送14)SIGALRM訊號,進程收到該訊號,默認動作終止。每個進程都有且只有唯一的一個定時器。
     取消定時器alarm(0),返回舊鬧鐘餘下秒數。
參數:
     seconds:指定的時間,以秒為單位
返回值:
     返回0或剩餘的秒數
  • 定時,與進程狀態無關(自然定時)就緒、運行、掛起(阻塞,暫停)、終止、殭屍…….無論進程處於何種狀態,alarm都計時。

程式碼示例:

#include<stdio.h>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>
 int main()
 {  
   unsigned int ret = 0;
   //第一次設置鬧鐘5秒之後就超時 發送對應的訊號
   ret = alarm(5);
   printf("上一次鬧鐘剩下的時間是%u\n",ret);
   sleep(2);
   //之前沒有超時的鬧鐘被新的鬧鐘給覆蓋
   ret = alarm(4);
   printf("上一次鬧鐘剩下的時間是%u\n",ret);
   printf("按下任意鍵繼續...");
   getchar();
   return 0;
 }

運行結果:

通過硬體異常產生

硬體異常被硬體以某種方式被硬體檢測到並通知內核,然後內核向當前進程發送適當的訊號。
這裡給大家介紹兩個硬體異常:CPU產生異常MMU產生異常

CPU產生異常 發生除零錯誤,CPU運行單元會產生異常,內核將這個異常解釋為訊號,最後OS發送SIGFPE訊號給進程。

程式碼示例

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
int main()
{
  // 由軟體條件產生訊號  alarm函數和SIGPIPE
  // CPU運算單元產生異常,內核將這個異常處理為SIGFPE訊號發送給進程
  int a = 10;
  int b = 0;
  printf("%d", a/b); 
  return 0;
}

運行結果小夥伴們自己下去運行一下吧~這裡就不截圖了

MMU產生異常: 當進程訪問非法地址時,mmu想通過頁表映射來將虛擬轉換為物理地址,此時發現頁表中不存在該虛擬地址,此時會產生異常,然後OS將異常解釋為SIGSEGV訊號,然後發送給進程

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
int main()
{
  // MMU硬體產生異常,內核將這個異常處理為SIGSEGV訊號發送給進程
  signal(11, handler);
  int* p = NULL;
  printf("%d\n", *p);
  return 0;
}

signal函數實現訊號的捕捉

#include<signal.h>
typedef void(*sighandler_t)(int);//它定義了一個類型sighandler_t,表示指向返回值為void型(參數為int型)的函數(的)指針
sighandler_t signal(int signum,sighandler_t handler);
功能:
    註冊訊號處理函數(不可用於SIGKILL、SIGSTOP訊號),即確定收到訊號後處理函數的入口地址。此函數不會阻塞。
參數:
    signum:訊號的編號,這裡可以填數字編號,也可以填訊號的宏義。
    handler:取值有3種情況:
             SIG_IGN:忽略該訊號
             SIG_DFL:執行系統默認動作
             訊號處理函數名:自定義訊號處理函數,如:func
             回調函數的定義如下:
             void func(int signo)
             {
             	//signo為觸發的訊號,為signal()第一個參數的值
             }

返回值:
    成功:第一次返回NULL,下一次返回此訊號上一次註冊的訊號處理函數的地址。如果需要使用此返回值,必須在前面先聲明此函數指針的類型。
    失敗:返回SIG_ERR

程式碼示例:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
//訊號處理函數
void fun1(int signum)
{
    printf("捕捉到訊號:%d\n",signum);
}
//訊號處理函數2
void fun2(int signum)
{
    printf("捕捉到訊號:%d\n",signum);
}
//訊號註冊函數
int main()
{
    //訊號註冊
    //ctrl+c
    signal(SIGINT,fun1); 
    //ctrl+\
    signal(SIGQUIT,fun2);
    while(1)
    {
      sleep(1);
    }
    return 0;
}

運行結果如下:

訊號先註冊,進程不要退出,然後等待訊號的到達,訊號到達之後就會執行訊號處理函數。

阻塞訊號

了解幾個概念:

  • 實際執行訊號的處理動作稱為訊號遞達

  • 訊號遞達的三種方式:默認、忽略和自定義捕捉

  • 訊號從產生到遞達之間的狀態,稱為訊號未決(Pending)

  • 進程可以選擇阻塞 (Block )某個訊號

  • 被阻塞的訊號產生時將保持在未決狀態,直到進程解除對此訊號的阻塞,才執行遞達的動作

    注意

    • 阻塞和忽略是不同的,只要訊號被阻塞就不會遞達,而忽略是在遞達之後可選的一種處理動作
    • OS發生訊號給一個進程,此訊號不是立即被處理的,那麼這個時間窗口中,訊號就需要被記錄保存下來,那麼訊號是如何在內核中保存和表示的呢?

每個進程都存在一個PCB,在PCB中存在兩個集合,一個是未決訊號集,一個是阻塞訊號集。

以SIGINT為例說明未決訊號集和阻塞訊號集的關係:

  • 當進程收到SIGINT訊號(訊號編號為2),首先這個訊號會保存在未決訊號集合中,此時對應的2號編號的這個位置上置為1,表示處於未決狀態;在這個訊號需要被處理之前首先要在阻塞訊號集中的編號2的位置上區檢查該值是否為1
  • 如果是1,表示SIGINT訊號被當前進程阻塞了,這個訊號暫時不被處理,所以未決集上該位置上的值保持為1,表示該訊號處於未決狀態
  • 如果是0,表示SIGINT訊號沒有被當前進程阻塞,這個訊號需要被處理,內核會對SIGINT訊號進行處理(執行默認動作,忽略或者執行用戶自定義的訊號處理函數),並將未決訊號集合中編號2的位置將1置為0,表示該訊號已經被處理了,這個時間非常短
  • 當SIGINT訊號從阻塞訊號集中解除阻塞之後,該訊號就會被處理

注意:

未決訊號集在內核中,要對內核進行操作只能通過系統調用,但是沒有提供這樣的方法,所以只能對未決訊號集進行讀操作,但是可以對阻塞訊號集進行讀寫操作。

問題1:所有訊號的產生都要由OS來進行執行,這是為什麼?

訊號的產生涉及到軟硬體,且OS是軟硬體資源的管理者,還是進程的管理者。

問題2:進程在沒有收到訊號的時候,能否知道自己應該如何對合法訊號進行處理呢?

答案是能知道的。每個進程都可以通過task_struct找到表示訊號的三張表。此時該進程的未決訊號集表中哪些訊號對應的那一位比特位是為0的,且進程能夠查看阻塞訊號集表知道如果收到該訊號是否需要阻塞,可以查看handler表知道對該訊號的處理動作。

問題3:OS如何發生訊號?

OS給某一個進程發送了某一個訊號後,OS會找到訊號在進程中未決訊號集表對應的那一位比特位,然後把那一位比特位由0置1,這樣OS就完成了訊號發送的過程。

訊號集操作函數

sigset_t: 未決和阻塞標誌可以用相同的數據類型sigset_t來存儲,sigset_t稱為訊號集,也被定義為一種數據類型。這個類型可以表示每個訊號狀態處於何種狀態(是否被阻塞,是否處於未決狀態)。阻塞訊號集也叫做當前進程的訊號屏蔽字,這裡的「屏蔽」應該理解為阻塞而不是忽略。

實際上兩個訊號集在都是內核使用點陣圖機制來實現的,想了解的可以自己去了解下,但是作業系統不允許我們直接對其操作。而需要自定義另外一個集合,藉助於訊號集操作函數來對PCB中的這兩個訊號集進行修改。

訊號集操作函數: sigset_t類型對於每種訊號用一個bit表示「有效」或「無效」狀態,至於這個類型內部如何存儲這些bit則依賴於系統實現,從使用者的角度是不必關心的,使用者只能調用以下函數來操作sigset_ t變數,而不應該對它的內部數據做任何解釋。
注意: 對應sigset類型的變數,我們不可以直接使用位操作來進行操作,而是一個嚴格實現系統給我們提供的庫函數來對這個類型的變數進行操作。

下面是訊號集操作函數的原型:

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
  • sigemptyset: 初始化set指向的訊號集,將所有比特位置0
  • sigfillset: 初始化set指向的訊號集,將所有比特位置1
  • sigaddset: 把set指向的訊號集中signum訊號對應的比特位置1
  • sigdelset: 把set指向的訊號集中signum訊號對應的比特位置0
  • sigismember: 判斷signum訊號是否存在set指向的訊號集中(本質是訊號判斷對應位是否為1)

注意: 在實現這些函數之前,需要使用sigemptysetsigfillset對訊號集進行初始化。前四個函數的返回值是成功返回0,失敗返回-1。最後一個函數的返回值是真返回1,假返回-1

阻塞訊號集操作函數——sigprocmask:

#include<signal.h>
int sigprocmask(int how,const sigset_t *set,sigset_t *oldset);
功能:
    檢查或修改訊號阻塞集,根據how指定的方法對進程的阻塞集合進行修改,新的訊號阻塞集由set指定,而原先的訊號阻塞集合由oldset保存。
參數:
    how:訊號阻塞集合的修改方法,有3種情況:
    SIG_BLOCK:向訊號阻塞集合中添加set訊號集,新的訊號掩碼是set和舊訊號掩碼的並集。相當於mask=mask丨set。
    SIG_UNBLOCK:從訊號阻塞集合中刪除set訊號集,從當前訊號掩碼中去除set中的訊號。相當於mask=mask&~set。
    SIG_SETMASK:將訊號阻塞集合設為set訊號集,相當於原來訊號阻塞集的內容清空,然後按照set中的訊號重新設置訊號阻塞集。相當於mask=set。
    set:要操作的訊號集地址。
         若set為NULL,則不改變訊號阻塞集合,函數只把當前訊號阻塞集合保存到oldset中。
    oldset: 保存原先訊號阻塞集地址。

返回值:成功:0  失敗:-1

未決訊號集操作函數——sigpending:

#include<signal.h>

int sigpending(sigset_t *set);
功能:讀取當前進程的未決訊號集
參數:
    set:未決訊號集
返回值:
    成功:0  失敗:-1

程式碼示例:

實驗一:把進程中訊號屏蔽字2號訊號進行阻塞,然後每隔1s對未決訊號集進行列印,觀察現象。

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void PrintPending(sigset_t* pend)
{
  int i = 0;
  for (i = 1; i < 32; ++i)
  {
    if (sigismember(pend, i)){
      printf("1");
    }
    else{
      printf("0");
    }
  }
  printf("\n");
}
int main()
{
  sigset_t set, oset;
  sigset_t pending;
  // 使用系統函數對訊號集進行初始化
  sigemptyset(&set);
  sigemptyset(&oset);
  sigemptyset(&pending);

  // 阻塞2號訊號
  // 先用系統函數對set訊號集進行設置
  sigaddset(&set, 2);
  // 使用sigprocmask函數更改進程的訊號屏蔽字
  // 第一個參數,三個選項:SIG_BLOCK(mask |= set) SIG_UNBLOCK(mask &= ~set) SIG_SETMASK(mask = set)
  sigprocmask(SIG_BLOCK, &set, &oset);
  
  int flag = 1; // 表示已經阻塞2號訊號
  int count = 0;
  while (1){
    // 使用sigpending函數獲取pending訊號集
    sigpending(&pending);
    // 列印pending點陣圖
    PrintPending(&pending);
    sleep(1);
  }
  return 0;
}

運行結果如下:

可以看到,進程收到2號訊號時,且該訊號被阻塞,處於未決狀態,未決訊號集中2號訊號對應的比特位由0置1

實例2: 將上面的程式碼進行修改,進行運行10s後,我們將訊號屏蔽字中2號訊號解除屏蔽

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void PrintPending(sigset_t* pend)
{
  int i = 0;
  for (i = 1; i < 32; ++i)
  {
    if (sigismember(pend, i)){
      printf("1");
    }
    else{
      printf("0");
    }
  }
  printf("\n");
}
int main()
{
  sigset_t set, oset;
  sigset_t pending;
  // 使用系統函數對訊號集進行初始化
  sigemptyset(&set);
  sigemptyset(&oset);
  sigemptyset(&pending);

  // 阻塞2號訊號
  // 先用系統函數對set訊號集進行設置
  sigaddset(&set, 2);
  // 使用sigprocmask函數更改進程的訊號屏蔽字
  // 第一個參數,三個選項:SIG_BLOCK(mask |= set) SIG_UNBLOCK(mask &= ~set) SIG_SETMASK(mask = set)
  sigprocmask(SIG_BLOCK, &set, &oset);
  
  int flag = 1; // 表示已經阻塞2號訊號
  int count = 0;
  while (1){
    // 使用sigpending函數獲取pending訊號集
	sigpending(&pending);
	// 列印pending點陣圖
	PrintPending(&pending);
	if (++count == 10){
		// 兩種方法都可以
		sigprocmask(SIG_UNBLOCK, &set, &oset);
		//sigprocmask(SIG_SETMASK, &oset, NULL);
	}
sleep(1);
  }
  return 0;
}

運行結果如下:

訊號2被阻塞之後就變成了未決狀態,當該訊號從阻塞集合中解除的時候,該訊號就會被處理,該訊號被處理後,該訊號的未決訊號集的標誌位將從1置為0。

訊號的捕獲

一個進程收到一個訊號的時候,可以用以下的方法進行處理:

(1)執行系統默認動作:對大多數訊號來說,系統默認動作就是來終止進行。

(2)忽略此訊號(丟棄):接收到此訊號後沒有任何動作

(3)執行自定義訊號處理函數(捕獲):用戶定義的訊號處理函數處理該訊號

注意:SIGKILL和SIGSTOP不能更改訊號的處理方式,因為它們向用戶提供了一種使進程終止的可靠方法。

訊號捕捉的過程

先思考一個問題:訊號是什麼時候被進程處理的?

首先,不是立即被處理的。而是在合適的時候,這個合適的時候,具體指的是進程從用戶態切換回內核態時進行處理

這句話如何理解,什麼是用戶態?什麼是內核態?

  • 用戶態: 處於⽤戶態的 CPU 只能受限的訪問記憶體,用戶的程式碼,並且不允許訪問外圍設備,許可權比較低
  • 內核態: 處於內核態的 CPU 可以訪問任意的數據,包括外圍設備,⽐如⽹卡、硬碟等,許可權比較高

注意: 作業系統中有一個cr暫存器來記錄當前進程處於何種狀態

進程空間分為用戶空間和內核空間。此前我們介紹的頁表都是用戶級頁表,其實還有內核級頁表。進程的用戶空間是通過用戶級頁表映射到物理記憶體上,內核空間是通過內核級頁表映射到物理記憶體上,如下面簡圖所示:


  • 當進程運行在內核空間時就處於內核態,而進程運行在用戶空間時則處於用戶態。

  • 最高 1G 的內核空間是被所有進程共享的!

進程有不同的用戶空間,但是只有一個內核空間,不同進程的用戶空間的程式碼和數據是不一樣的,但是內核空間的程式碼和數據是一樣的。
上面這些主要是想說:進程處於用戶態訪問的是用戶空間的程式碼和數據,進程處於內核態,訪問的是內核空間的程式碼和數據。

訊號捕捉的整個過程:

從上面的圖可以看出,進程是在返回用戶態之前對訊號進行檢測,檢測pending點陣圖,根據訊號處理動作,來對訊號進行處理。這個處理動作是在內核態返回用戶態後進行執行的,所以這裡也就回答了開始提出的那一個問題了。

sigaction函數

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
功能:
   檢查或修改指定訊號的設置(或同時執行兩種操作)
參數:
   signum:要操作的函數
   act:要設置的對訊號的新處理方式(傳入方式)
   oldact:原來對訊號的處理方式(傳出參數)
   
   如果act指針非空,則要改變指定訊號的處理(設置),如果oldact指針非空,則系統將此前指定訊號的處理方式入oldact
返回值:
     成功:0
     失敗:-1

struct sigaction結構體:

struct sigaction {
	void     (*sa_handler)(int);//舊的訊號處理函數指針
    void     (*sa_sigaction)(int, siginfo_t *, void *);//新的訊號處理函數指針
    sigset_t   sa_mask;//訊號阻塞集
    int        sa_flags;//訊號處理的方式
    void     (*sa_restorer)(void);//已經棄用
};

(1)sa_handler、sa_sigaction:訊號處理函數指針,和signal裡面的函數指針用法是一樣的,根據情況給兩個指針賦值。

​ a)SIG_IGN:忽略該訊號

​ b)SIG_DFL:執行系統默認的動作

​ c)處理函數名:自定義訊號處理函數

(2)sa_mask:訊號阻塞集,在執行訊號處理函數的時候,用來臨時的屏蔽訊號

(3)sa_flags:用於指定訊號處理的行為,通常設置為0,表示使用默認的屬性。它可以是一下值的「按位」或「組合」:

  • SA_NOCLDSTOP:使父進程在它的子進程暫停或繼續運行時不會收到SIGCHLD訊號。
  • SA_NOCLDWAIT:使父進程在它的子進程退出的時候不會收到SIGCHLD訊號,這時子進程如果退出也不會成為殭屍進程。
  • SA_NODEFER:使對訊號的屏蔽無效,即在訊號處理函數執行期間仍能發出整個訊號。
  • SA_RESETHAND:訊號處理之後重新設置為默認的處理方式。
  • SA_SIGINFO:使用sa_sigaction成員而不是sa_handler作為訊號處理函數。

程式碼示例:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handler(int signo)
{
  printf("catch a signal: %d\n", signo);
}
int main()
{
  struct sigaction act, oact;

  act.sa_flags = 0;// 選項 設置為0
  sigfillset(&act.sa_mask);
  act.sa_handler = handler;
  // 對2號訊號修改處理動作
  sigaction(2, &act, &oact);
  while (1){
    raise(2);
    sleep(1);
  }
  return 0;
}

運行結果如下:

程式碼示例:舊的訊號處理函數

sa_flags標誌為0代表使用的是舊的訊號處理函數

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void handler(int signo)
{
    printf("catch a signal: %d\n", signo);
}
int main()
{
    int ret = -1;
    struct sigaction act;
    //標誌為0,代表使用的是舊的訊號處理函數指針
    act.sa_flags = 0;
    //給阻塞集初始化
    sigfillset(&act.sa_mask);
    act.sa_handler = handler;
    // 訊號註冊
    ret =  sigaction(SIGINT, &act, NULL);
    if(ret == -1)
    {
      perror("sigaction");
      return 1;
    }
    printf("按下任意鍵退出.....\n");
    getchar();
    return 0;
}

運行結果如下:

程式碼示例:新的訊號處理函數

#include <stdio.h>
#include <unistd.h>
#include <signal.h> 
void handler(int signo,siginfo_t *info,void *context)
{
     printf("catch a signal: %d\n", signo);
} 
int main()
  {
    int ret = -1;
    struct sigaction act;
    //使用新的訊號處理函數指針
    act.sa_flags = 0;
    //給阻塞集初始化
    sigfillset(&act.sa_mask);
    act.sa_handler = handler;
    // 訊號註冊
    ret =  sigaction(SIGINT, &act, NULL);
    if(ret == -1)
    {
      perror("sigaction");
      return 1;
    }
    printf("按下任意鍵退出.....\n");
    getchar();
    return 0;
 }

不可重入函數和可重入函數

先看下面一段程式碼:

#include <stdio.h>
#include <signal.h>

int a = 10;

void SelfAdd(int n)
{
	a = a + n;
	a = a + n;
}

void handler(int signo)
{
	SelfAdd(signo);
}
int main()
{
	signal(2, handler);
	SlefAdd(2);
	printf("%d\n", a);
	return 0;
}

上面我寫了一個比較簡單的程式碼,我們慢慢分析,當我們在主函數中執行調用SelfAdd時,進入該函數,執行完函數中int a = a + n這句程式碼後,a變成了12,此時收到2號訊號,發生中斷

最後列印a結果是16,其實正常調用該函數的話,列印的應該是18。
像上面這樣的因為重入導致結果錯亂的函數就叫做不可重入函數。其中a是一個全局變數。如果一個函數值訪問自己的局部變數或參數,那麼這樣的函數就叫做可重入函數。

說的通俗點,不可重入的意思是,如果你定義了一個全局變數,在函數1裡面這個變數應該是10,但是有一個函數2改變了這個變數的值,此時本來函數1用的是10 ,你把他改變了,這就是不安全的,這就是不可重入函數。

思考一個問題:為什麼兩個不同的控制流程調用同一個函數,訪問同一個局部變數或參數不會造成錯亂?

在多執行緒中,每個執行緒雖然是資源共享,但是他們的棧卻是獨有的,所以說局部變數不會造成錯亂。

如果一個函數符合以下條件之一則是不可重入的:

  • 調用了malloc或free,因為malloc也是用全局鏈表來管理堆的。

  • 調用了標準I/O庫函數。標準I/O庫的很多實現都以不可重入的方式使用全局數據結構。

  • 函數體內使用了靜態的數據結構。

保證函數可重入性的方法:

  • 在寫函數的時候盡量使用局部變數。(例如暫存器和棧中的變數)
  • 對於要使用的全局變數要加以保護(採取中斷、訊號量互斥的方法),這樣構成的函數就一定是可重入函數。

使用訊號避免殭屍進程

SIGCHLD訊號

產生條件:1)子進程終止時

​ 2)子進程接收到SIGSTOP訊號停止的時候

​ 3)子進程處於停止態,接收到SIGCONT後喚醒時

如何避免殭屍進程:

1)最簡單的方法,父進程通過wait和waitpid等待子進程函數結束,但是,這會導致父進程掛起。

2)如果父進程要處理的事情很多,不能掛起,通過signal()函數人為處理訊號SIGCHLD,只有在子進程退出自動調用制定好的回調函數,因為子進程結束後,父進程會收到訊號SIGCHLD,可以在回調函數裡面用wait或waitpid回收資源。

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include<sys/types.h>
#include<sys/wait.h>
void sig_child(int signo)
{
	pid_t pid;
	//處理殭屍進程,-1代表等待任意一個子進程,WNOHANG代表不阻塞
	while((pid=waitpid(-1,NULL,WNOHANG))>0)
	{
		printf("孩子進程被殺死 %d\n",pid);
	}
}
int main()
{
     pid_t pid;
     //創建捕捉子進程退出訊號
     //只要子進程退出,觸發SIGSIGCHLD,自動調用sig_child()
     signal(SIGCHLD,sig_child());
     //創建進程
     pid = fork();
     if(pid<0)
     {
        perror("fork");
        exit(1);
     }
     else if(pid == 0)
     {
        //子進程
        printf("我是子進程,pid id :%d.我正在退出\n",getpid());
        exit(0);
     }
else if(pid>0)
     {
        //父進程
        sleep(2);//保證子進程先運行
        printf("我是父親,我正在退出\n");
        system("ps -ef|grep defunct");//查看有沒有殭屍進程
     }
    return 0;
}

運行結果:

3)如果父進程不關心子進程時候結束,那麼可以用signal(SIGCHLD,SIG_IGN)通知內核,自己對子進程的結束不感興趣,父進程忽略此訊號,那麼子進程結束後,內核會回收,並不再給父進程發送訊號。

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
     pid_t pid;
     //忽略子進程退出訊號的訊號
     //那麼子進程結束之後,內核會回收,並不再給父進程發送訊號
     signal(SIGCHLD,SIG_IGN);
     //創建進程
     pid = fork();
     if(pid<0)
     {
        perror("fork");
        exit(1);
     }
     else if(pid == 0)
     {
        //子進程
        printf("我是子進程,pid id :%d.我正在退出\n",getpid());
        exit(0);
     }
else if(pid>0)
     {
        //父進程
        sleep(2);//保證子進程先運行
        printf("我是父親,我正在退出\n");
        system("ps -ef|grep defunct");//查看有沒有殭屍進程
     }
    return 0;
}

運行結果: