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

運行結果: