1. 程式人生 > >【Linux】訊號的產生、阻塞與捕捉

【Linux】訊號的產生、阻塞與捕捉

文章目錄

1.訊號的基本概念

1)理解什麼是訊號?

  1. ⽤戶輸⼊命令,在Shell下啟動⼀個前臺程序。

  2. ⽤戶按下Ctrl-C,這個鍵盤輸⼊產⽣⼀個硬體中斷。

  3. 如果CPU當前正在執⾏這個程序的程式碼,則該程序的⽤戶空間程式碼暫停執⾏,CPU從⽤戶態 切換到核心態處理硬體中斷。

  4. 終端驅動程式將Ctrl-C解釋成⼀個SIGINT訊號,記在該程序的PCB中(也可以說傳送了⼀ 個SIGINT訊號給該程序)。(說明PCB可記錄訊號,是引用點陣圖記錄,位置訊號的1/0表明訊號的存在與否)(只有OS有資格修改目標程序的點陣圖資訊也只有OS能傳送,發訊號其實更準確來說是寫訊號)。

  5. 當某個時刻要從核心返回到該程序的⽤戶空間程式碼繼續執⾏之前,⾸先處理PCB中記錄的訊號,發現有⼀個SIGINT訊號待處理,⽽這個訊號的預設處理動作是終⽌程序,所以直接終⽌程序⽽不再返回它的⽤戶空間程式碼執⾏。
    注意

  6. Ctrl-C產⽣的訊號只能發給前臺程序。⼀個命令 後⾯加個&可以放到後臺運⾏,這樣Shell不必等待程序結束就可以接受新的命令,啟動新的程序。

  7. Shell可以同時運⾏⼀個前臺程序和任意多個後臺程序,只有前臺程序才能接到像Ctrl-C這種控制鍵產⽣的訊號。

  8. 前臺程序在運⾏過程中⽤戶隨時可能按下Ctrl-C⽽產⽣⼀個訊號,也就是說該程序的⽤戶空間程式碼執⾏到任何地⽅都有可能收到SIGINT訊號⽽終⽌,所以訊號相對於程序的控制流程來說是非同步(Asynchronous)的。
    總結

    1.程序要處理訊號必須要認識訊號(點陣圖加規定)
    2.訊號產生程序收到後有可能不立刻處理(先儲存)而在合適的時候
    3.訊號機制的產生對程序來說是非同步的執行流並沒有關係
    4.訊號處理的三種方法:執行預設訊號/忽略/自定義
    5.如果當前訊號不能被處理就先記錄。

2)訊號列表

使用命令:kill -l 可以檢視系統定義的訊號列表。
在這裡插入圖片描述
每個訊號都有⼀個編號和⼀個巨集定義名稱,這些巨集定義可以在signal.h中找到,例如其中有定義 #define SIGINT 2 。 1-31為普通訊號,34-64為實時訊號。這些訊號各自在什麼條件下產生,預設的處理動作是什麼,在signal(7)中都有詳細說明:man 7 signal

2.訊號的產生

1)產生訊號的方法概述

《1》終端產生
Ctrl+c產生SIGINT訊號
Ctrl+\產生SIGQUIT訊號
Ctrl+Z產生SIGTSTP訊號
《2》硬體異常產生
,這些條件由硬體檢測到並通知核心,然後核心向當前程序傳送適當的信 號。例如當前程序執⾏了除以0的指令,CPU的運算單元會產⽣異常,核心將這個異常解釋 為SIGFPE訊號傳送給程序。再⽐如當前程序訪問了⾮法記憶體地址,MMU會產⽣異常,核心 將這個異常解釋為SIGSEGV訊號傳送給程序。
《3》kill產生
⼀個程序調⽤kill(2)函式可以傳送訊號給另⼀個程序。 可以⽤kill(1)命令傳送訊號給某個程序,kill(1)命令也是調⽤kill(2)函式實現的,如果不明確指定訊號則傳送SIGTERM訊號,該訊號的預設處理動作是終⽌程序。 當核心檢測到某種軟體條件發⽣時也可以通過訊號通知程序,例如鬧鐘超時產SIGALRM訊號,向讀端已關閉的管道寫資料時產⽣SIGPIPE訊號。 如果不想按預設動作處理訊號,⽤戶程式可以調⽤sigaction(2)函式告訴核心如何處理某種訊號.
《4》軟體條件產生

2)終端產生訊號

SIGINT的預設處理動作是終⽌程序,SIGQUIT的預設處理動作是終⽌程序並且Core Dump,現在我們來驗證⼀下。
Core Dump
⾸先解釋什麼是Core Dump。當⼀個程序要異常終⽌時,可以選擇把程序的⽤戶空間記憶體資料全部 儲存到磁碟上,⽂件名通常是core,這叫做Core Dump。程序異常終⽌通常是因為有Bug,⽐如⾮法記憶體訪問導致段錯誤,事後可以⽤偵錯程式檢查core⽂件以查清錯誤原因,這叫做Post-mortemDebug(事後除錯)。
⼀個程序允許產⽣多⼤的core⽂件取決於程序的Resource Limit(這個資訊儲存 在PCB中)。預設是不允許產⽣core⽂件的,因為core⽂件中可能包含⽤戶密碼等敏感資訊,不安全。在開發除錯階段可以⽤ulimit命令改變這個限制,允許產⽣core⽂件。

 $ ulimit -c 1024//修改core檔案允許產生最大1024k
 $ ulimit -a //檢視資源

事後除錯:

  • $ ulimit -c 1024
  • 寫一個死迴圈程式,使用ctrl + \將它終止掉,ls發現會多一個core.檔案。
    在這裡插入圖片描述
  • 使用gdb除錯,core-file core.檔案。
  • core檔案直接幫我們定位出來錯誤的地方。在這裡插入圖片描述

3)呼叫系統函式向程序發訊號

《1》kill命令

kill命令是調⽤kill函式實現的。kill函式可以給⼀個指定的程序傳送指定的訊號。

#include<signal.h>
int kill(pid_t pid,int signo);//kill命令的呼叫kill函式實現的
返回值:成功返回0,失敗返回-1

⾸先在後臺執⾏死迴圈程式
在這裡插入圖片描述

然後⽤kill命令給它發SIGSEGV訊號

在這裡插入圖片描述
之所以要再次回⻋才顯⽰ Segmentation fault ,是因為在13972程序終⽌
掉 之前已經回到了Shell提⽰符等待⽤戶輸⼊下⼀條命令,Shell不希望Segmentation fault資訊和⽤戶的輸⼊交錯在⼀起,所以等⽤戶輸⼊命令之後才顯⽰。

kill -SIGSEGV  程序PID與`kill -11 程序PID(11是SIGSEGV的編號)` 相同。

以往遇 到的段錯誤都是由⾮法記憶體訪問產⽣的,⽽這個程式本⾝沒錯,給它發SIGSEGV也能產⽣段錯誤

《2》raise函式

給當前程序傳送指定訊號(自己給自己發)

#include<signal.h>
int raise(int signo);
返回值:成功返回0,失敗返回-1

《3》abort函式

使當前程序接收到訊號⽽異常終⽌

#include <stdlib.h>
void abort(void);
就像exit函式⼀樣,abort函式總是會成功的,所以沒有返回值。

4)軟體條件產生訊號

SIGPIPE是⼀種由軟體條件產⽣的訊號,當讀端將自己的檔案描述符關閉,OS會向寫端傳送一個SIGPIPE訊號來關閉寫端。

《1》alarm函式

#include<unistd.h>
unsigned int alarm(unsigned int seconds);
功能:設定一個鬧鐘,告訴核心在seconds秒後給當前程序傳送SIGALRM訊號,該訊號的預設動作是終止程序

這個函式的返回值是0或者是以前設定的鬧鐘時間還餘下的秒數。

eg:下面這個程式作用是2秒鐘之內不停的數數,2秒鐘到了就被SIGALRM訊號終⽌.

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

int main()
{
  int count = 0;
  alarm(2);
  for(count = 0;2;count++){
    printf("count = %d\n",count);
  }
  return 0;
}

《2》signal函式

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
//signum是訊號編號,
//handler是一個函式指標,這個函式指標指向的是對預設處理替換的方法函式

3.阻塞訊號

1)訊號其他相關常見概念

  • 實際執行訊號的處理動作稱為訊號遞達(Delivery)(要遞達絕對沒有阻塞)
  • 訊號從產生到遞達之間的狀態,稱為訊號未決(Pending)

pending->block->阻塞 pending->未block->合適時遞達

  • 程序可以選擇阻塞(Block)某個訊號。(阻塞也叫做遮蔽)沒處理除非解除阻塞。
  • 被阻塞的訊號產生時將保持在未決狀態,直到程序解除對此洗腦的阻塞,才執行遞達的動作。
  • 阻塞與忽略是不同的,只要訊號被阻塞就不會遞達,而忽略是在遞達之後,可選的一種處理動作。(忽略表明處理了)

2)在核心中的表達

訊號在核心中的表示示意圖:(點陣圖儲存)
在這裡插入圖片描述
由上圖知

block的0/1表明是否被遮蔽。
pending0/1表明訊號是否存在。訊號產生時,核心在程序控制塊中的未決標誌直到訊號遞達才清除該標誌。圖中的2號訊號還沒有被處理。
handler表示處理動作:函式指標陣列,先編號找到位置,再將函式指標放入。
如果在程序解除對某訊號的阻塞之前這種訊號產⽣過多次,將如何處理?POSIX.1允許系統遞送該訊號⼀次或多次。Linux是這樣實現的:常規訊號在遞達之前產⽣多次只計⼀
次,⽽實時訊號在遞達之前產⽣多次可以依次放在⼀個佇列⾥。

3)sigset_t

點陣圖,包含block表和pending表。 每個訊號只有⼀個bit的未決標誌,⾮0即1,不記錄該訊號產⽣了多少次,阻塞標誌也是這樣表⽰的。 因此,未決和阻塞標誌可以⽤相同的資料型別sigsett來儲存,sigsett稱為訊號集,這個型別可以表⽰每個訊號的“有效”或“⽆效”狀態,在阻塞訊號集中“有效”和“⽆效”的含義是該訊號是否被阻塞,⽽在未決信
號集中“有效”和“⽆效”的含義是該訊號是否處於未決狀態。 阻塞訊號集也叫做當前程序的訊號遮蔽字(Signal Mask),這⾥的“遮蔽”應該理解為阻塞⽽不是忽略。

4)訊號集操作函式

sigsett型別對於每種訊號⽤⼀個bit表⽰“有效”或“⽆效”狀態,⾄於這個型別內部如何儲存這些bit則依賴於系統實現,從使⽤者的⾓度是不必關⼼的,使⽤者只能調⽤以下函式來操作sigset t變數,⽽不應該對它的內部資料做任何解釋,⽐如⽤printf直接列印sigset_t變數是沒有意義的。

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
  • 函式sigemptyset初始化set所指向的訊號集,使其中所有訊號的對應bit清零,表⽰該訊號集不包含 任何有效訊號。
  • 函式sigfillset初始化set所指向的訊號集,使其中所有訊號的對應bit置位,表⽰ 該訊號集的有效訊號包括系統⽀持的所有訊號。
  • 注意,在使⽤sigset_ t型別的變數之前,⼀定要調 ⽤sigemptyset或sigfillset做初始化,使訊號集處於確定的狀態。初始化sigset_t變數之後就可以在調⽤sigaddset和sigdelset在該訊號集中新增或刪除某種有效訊號。

這四個函式都是成功返回0,出錯返回-1。sigismember是⼀個布林函式,⽤於判斷⼀個訊號集的有效訊號中是否包含某種 訊號,若包含則返回1,不包含則返回0,出錯返回-1。

5)sigprocmask函式

可以讀取或更改程序的訊號遮蔽字(阻塞訊號集)。

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功則為0,若出錯則為-1 

如果oset是⾮空指標,則讀取程序的當前訊號遮蔽字通過oset引數傳出。
如果set是⾮空指標,則 更改程序的訊號遮蔽字,引數how指⽰如何更改。
如果oset和set都是⾮空指標,則先將原來的訊號 遮蔽字備份到oset⾥,然後根據set和how引數更改訊號遮蔽字。
假設當前的訊號遮蔽字為mask,下表說明了how引數的可選值。
在這裡插入圖片描述
如果呼叫sigprocmask解除了對當前若干個未決訊號的阻塞,則在函式返回之前,至少將其中一個訊號遞達。

6)sigpending函式

讀取當前程序的未決訊號集,通過set引數傳出。呼叫成功則返回0,出錯則返回-1.

#include<signal.h>
int sigpending(sigset_t *set);

下面我們使用下剛才學習的函式:

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


void printsigset(sigset_t *set)
{
  int i = 0;
  for(;i<32;i++)
  {
    if(sigismember(set,i))
    {
      putchar('1');
    }
    else{
      putchar('0');
    }
  }
  puts("");
}

int main()
{
   sigset_t s,p;//定義訊號集物件,並清空初始化
   sigemptyset(&s);
   sigaddset(&s,SIGQUIT);
   sigprocmask(SIG_BLOCK,&s,NULL);//設定阻塞訊號集,阻塞SIGQUIT訊號
   while(1){
     sigpending(&p);//獲取未決訊號集
     printsigset(&p);
     sleep(1);
   }
}

執行該程式,每秒鐘把各訊號的未決狀態列印一遍,由於我們阻塞了SIGQUIT訊號,按Ctrl+\會使SIGQUIT處於未決狀態,如下圖所示。
在這裡插入圖片描述
使用Ctrl+C仍然可以終止程式,因為SIGINT訊號沒有阻塞。
在這裡插入圖片描述

4.捕捉訊號

1)訊號的捕捉

《1》首先我們先了解下核心態和使用者態的基本概念

  • 使用者態:使用者執行自身程式碼

  • 核心態:執行核心程式碼的狀態
    使用系統呼叫介面時可以由使用者態變為核心態,返回時再又核心態轉為使用者態,系統呼叫介面的存在就是為了保護OS,普通使用者不能訪問OS的程式碼資料。

    切入核心的方式除了系統呼叫還有中斷,異常,缺陷等方式

《2》核心如何實現訊號的捕捉

如果訊號的處理動作是使用者自定義函式,在訊號遞達時就呼叫這個函式,這就成為捕捉訊號。由於訊號處理函式的程式碼是在使用者空間的,處理過程比較複雜,舉例如下:使用者程式註冊了SIGQUIT 訊號的處理函式sighandler.當前正在執行main函式,這時發生中斷或者異常切換到核心態。在中斷處理完畢要返回使用者態的main函式之前檢查到有訊號SIGQUIT遞達。核心決定返回使用者態後不是恢復main函式的上下文繼續執行,而是執行sighandler函式,sighandler和main函式使用不同的堆疊空間,它們之間不存在呼叫和被呼叫的關係,如果沒有新的訊號要遞達,這次再返回使用者態就是恢復main函式的上下文繼續執行了。

 訊號處理在核心態切換至使用者態的時候,執行緒間的切換,程序間的切換也發生在核心態切換使用者態的期間。

在這裡插入圖片描述

《3》sigaction函式

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact); 
//const sigaction *act是指處理新的訊號
//struct sigaction *oact是將舊的返回(輸出型引數)
struct sigaction 
{
 void(*) (int)     sa_handler  ;
 sigset_t          sa_mask  ;
  int               sa_flags   ;
void(*) (int,  siginfo_t *,     void *)         sa_sigaction   ;

}

sigaction函式可以讀取和修改與指定訊號相關聯的處理動作。調⽤成功則返回0,出錯則返回- 1。signo是指定訊號的編號。若act指標⾮空,則根據act修改該訊號的處理動作。若oact指標⾮ 空,則通過oact傳出該訊號原來的處理動作。act和oact指向sigaction結構體:將sahandler賦值為常數SIGIGN傳給sigaction表⽰忽略訊號,賦值為常數SIG_DFL表⽰執⾏系統預設動作,賦值為⼀個函式指標表⽰⽤⾃定義函式捕捉訊號,或者說向核心註冊了⼀個訊號處理函 數,該函式返回值為void,可以帶⼀個int引數,通過引數可以得知當前訊號的編號,這樣就可以⽤同⼀個函式處理多種訊號。顯然,這也是⼀個回撥函式,不是被main函式調⽤,⽽是被系統所呼叫的。

當某個訊號的處理函式被調⽤時,核心⾃動將當前訊號加⼊程序的訊號遮蔽字,當訊號處理函式返回時⾃動恢復原來的訊號遮蔽字,這樣就保證了在處理某個訊號時,如果這種訊號再次產⽣,那麼 它會被阻塞到當前處理結束
為⽌。 如果在調⽤訊號處理函式時,除了當前訊號被⾃動遮蔽之外,還希望⾃動遮蔽另外⼀些訊號,則⽤samask欄位說明這些需要額外遮蔽的訊號,當訊號處理函式返回時⾃動恢復原來的訊號遮蔽字。 saflags欄位包含⼀些選項,saflags設為0 ,sasigaction是實時訊號的處理函式。

《4》pause

#include <unistd.h>
int pause(void);

pause函式使調⽤程序掛起直到有訊號遞達。如果訊號的處理動作是終⽌程序,則程序終 ⽌,pause函式沒有機會返回;如果訊號的處理動作是忽略,則程序繼續處於掛起狀態,pause不返回;如果訊號的處理動作是捕捉,則調
⽤了訊號處理函式之後pause返回-1,errno設定為EINTR, 所以pause只有錯的返回值(只有出錯返回值含有exec函式系列,程序函式替換)。錯誤碼EINTR表 ⽰“被訊號中斷”。

pause要有返回值則:1:收到訊號。2:自定義捕捉訊號。

下面我們用alarm和pause實現mysleep函式
思路

  • main 函式呼叫mysleep函式,然後呼叫sigaction註冊SIGALRM訊號的處理函式sig_alrm.
  • 呼叫alarm(nsecs)設為鬧鐘
  • 呼叫pause等待,核心切換到別的程序執行
  • nsecs秒後,鬧鐘超時,核心發SIGAKRM給這個程序
  • 從核心態返回這個程序的使用者態之前處理未決訊號,發現有SIGALRMB訊號被自動遮蔽,從從sig_alrm函式返回時SIGALRM訊號⾃動解除遮蔽。然後⾃動執⾏系統調⽤sigreturn再次進⼊ 核心,再返回⽤戶態繼續執⾏程序的主控制流程(main函式調⽤的mysleep函式)。
  • pause函式返回-1,然後調⽤alarm(0)取消鬧鐘,調⽤sigaction恢復SIGALRM訊號以前的處理動作。
    實現程式碼:
#include<stdio.h>
#include<signal.h>
#include<unistd.h>

void  sig_alrm(int signo)
{

}

unsigned int mysleep(unsigned  int  nsecs)
{
  struct sigaction  new,old;
  unsigned  int unslept = 0;
  new.sa_handler = sig_alrm;
  sigemptyset(&new.sa_mask);
  new.sa_flags = 0;
  sigaction(SIGALRM,&new,&old);//註冊訊號處理函式
  alarm(nsecs);//設定鬧鐘
  pause();
  unslept = alarm(0);//清空鬧鐘
  sigaction(SIGALRM,&old,NULL);//恢復訊號預設動作
  return unslept;
}



int main()
{
  while(1){
    mysleep(1);
    printf("1 seconds passed\n");
  }
  return 0;
}

2)可重入函式

《1》引入

我們以單鏈表的頭插為例:
在這裡插入圖片描述

main函式呼叫insert函式向一個連結串列head中插入結點node1,插入操作為兩步,剛做完第一步的時候,因為硬體中斷使程序切換到核心,再次回用戶態之前檢查到有訊號待處理,於是切換到sighandler函式,sighandler也呼叫insert函式向同⼀個連結串列head中插⼊節點node2,插⼊操作的 兩步都做完之後從sighandler返回核心態,再次回到⽤戶態就從main函式調⽤的insert函式中繼續 往下執⾏,先前做第⼀步之後被打斷,現在繼續做完第⼆步。結果是,main函式和sighandler先後 向連結串列中插⼊兩個節點,⽽最後只有⼀個節點真正插⼊連結串列中了。導致node2記憶體洩漏

《2》重入

上例中的insert函式被不同的控制流程呼叫,有可能在第一次呼叫還沒有返回時就再次進入該函式,這稱為重入。

《3》不可重入函式

上例中insert函式訪問一個全域性連結串列,有可能因為重入而造成錯亂,像這樣的函式稱為不可重入函式

不可重入函式的條件:

  • 呼叫了malloc或free,因為malloc也是用全域性連結串列來管理堆的
  • 呼叫了標準I/O庫函式。標準I/O庫的很多實現都以不可重入的方式使用全域性資料結構

《4》可重入函式

如果一個函式只訪問自己的區域性變數或引數則稱為可重入函式。

《5》多執行緒或多CPU程式設計要使用volatile關鍵字

對於程式中存在多個執⾏流程訪問同⼀全域性變數的情況,volatile限定符是必要的,此外,雖然程 序只有單⼀的執⾏流程,但是變數屬於以下情況之⼀的,也需要volatile限定:

1>變數的記憶體單元中的資料不需要寫操作就可以⾃⼰發⽣變化,每次讀上來的值都可能不⼀樣。
2>即使多次向變數的記憶體單元中寫資料,只寫不讀,也並不是在做⽆⽤功,⽽是有特殊意義的 。
什麼樣的記憶體單元會具有這樣的特性呢?肯定不是普通的記憶體,⽽是對映到記憶體地址空
間的硬體暫存器,例如串⼝的接收暫存器屬於上述第⼀種情況,⽽傳送暫存器屬於上述第⼆種情況。
3>.sig_ atomic_ t型別的變數應該總是加上volatile限定符,因為要使⽤sigatomict型別的理由也正 是要加volatile限定符的理由。

5.競態條件與sigsuspend函式

其實我們之前寫的mysleep函式還是有缺陷的,設想這樣的時序問題:

  1. 註冊SIGALRM訊號的處理函式。
  2. 調⽤alarm(nsecs)設定鬧鐘。
  3. 核心排程優先順序更⾼的程序取代當前程序執⾏,並且優先順序更⾼的程序有很多個,每個都要 執⾏很⻓時間
  4. nsecs秒鐘之後鬧鐘超時了,核心傳送SIGALRM訊號給這個程序,處於未決狀態。
  5. 優先順序更⾼的程序執⾏完了,核心要排程回這個程序執⾏。SIGALRM訊號遞達,執⾏處理函 數sig_alrm之後再次進⼊核心。
  6. 返回這個程序的主控制流程,alarm(nsecs)返回,調⽤pause()掛起等待。
  7. 可是SIGALRM訊號已經處理完了,還等待什麼呢?

出現這個問題的根本原因是系統運⾏的時序(Timing)並不像我們寫程式時所設想的那樣。雖然alarm(nsecs)緊接著的下⼀⾏就是pause(),但是⽆法保證pause()⼀定會在調⽤alarm(nsecs)之 後的nsecs秒之內被調⽤。由
於非同步事件在任何時候都有可能發⽣(這⾥的非同步事件指出現更⾼優 先級的程序),如果我們寫程式時考慮不周密,就可能由於時序問題⽽導致錯誤,這叫做競態條件

解決方式:
1)

1. 遮蔽SIGALRM訊號
2. alarm(nsecs)
3. 解除對SIGALRM訊號的遮蔽
4. pause();

上述過程相對靠譜,但是第三步在解除訊號遮蔽的時候,也可能立刻對遞達。
2)

1. 遮蔽SIGALRM訊號;
2. alarm(nsecs);
3. pause();
4. 解除對SIGALRM訊號的遮蔽

這樣更不⾏了,還沒有解除遮蔽就調⽤pause,pause根本不可能等到SIGALRM訊號。要是“解除訊號遮蔽”和“掛起等待訊號”這兩步能合併成⼀個原⼦操作就好了,這正是sigsuspend函式的功 能。sigsuspend包含了pause的掛起等待功能,同時解決了競態條件的問題,在對時序要求嚴格的場合下都應該調⽤sigsuspend⽽不是pause。

sigsuspend函式

#include <signal.h>
int sigsuspend(const sigset_t *sigmask);

和pause⼀樣,sigsuspend沒有成功返回值,只有執⾏了⼀個訊號處理函式之後sigsuspend才返回,返回值為-1,errno設定為EINTR。 調⽤sigsuspend時,程序的訊號遮蔽字由sigmask引數指定,可以通過指定sigmask來臨時解除對某 個訊號的遮蔽,然後掛起等待,當sigsuspend返回時,程序的訊號遮蔽字恢復為原來的值,如果原來對該訊號是遮蔽的從sigsuspend返回後仍然是遮蔽的。

接下來用sigsupend重新實現mysleep函式:

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

void  sig_alrm(int signo)
{

}

unsigned int mysleep(unsigned  int  nsecs)
{
  struct sigaction  new,old;
  sigset_t   newmask,oldmask,suspmask;
  unsigned  int unslept = 0;



  new.sa_handler = sig_alrm;
  sigemptyset(&new.sa_mask);
  new.sa_flags = 0;
  sigaction(SIGALRM,&new,&old);//註冊訊號處理函式


  sigemptyset(&newmask);
  sigaddset(&newmask,SIGALRM);
  sigprocmask(SIG_BLOCK,&newmask,&oldmask);



  alarm(nsecs);//設定鬧鐘

  suspmask = oldmask ;
  sigdelset(&suspmask,SIGALRM);
  sigsuspend(&suspmask);



  

  unslept = alarm(0);//清空鬧鐘
  sigaction(SIGALRM,&old,NULL);//恢復訊號預設動作
  sigprocmask(SIG_SETMASK,&oldmask,NULL);
  return unslept;
}



int main()
{
  while(1){
    mysleep(1);
    printf("1 seconds passed\n");
  }
  return 0;
}

如果在調⽤mysleep函式時SIGALRM訊號沒有遮蔽:

調⽤sigprocmask(SIG_ BLOCK, &newmask, &oldmask),遮蔽SIGALRM。
調⽤sigsuspend(&suspmask);解除對SIGALRM的遮蔽,然後掛起等待待。
SIGALRM遞達後suspend返回,⾃動恢復原來的遮蔽字,也就是再次遮蔽SIGALRM。
調⽤sigprocmask(SIG_ SETMASK, &oldmask, NULL);再次解除對SIGALRM的遮蔽。

6.SIGCHLD訊號

⽤wait和waitpid函式清理僵⼫程序,⽗程序可以阻塞等待⼦程序結束,也可以⾮阻 塞地查詢是否有⼦程序結束等待清理(也就是輪詢的⽅式)。採⽤第⼀種⽅式,⽗程序阻塞了就不 能處理⾃⼰的⼯作了;採⽤第⼆種⽅式,⽗程序在處理⾃⼰的⼯作的同時還要記得時不時地輪詢⼀ 下,程式實現複雜。
其實,⼦程序在終⽌時會給⽗程序發SIGCHLD訊號,該訊號的預設處理動作是忽略,⽗程序可以⾃ 定義SIGCHLD訊號的處理函式,這樣⽗程序只需專⼼處理⾃⼰的⼯作,不必關⼼⼦程序了,⼦程序 終⽌時會通知⽗程序,⽗程序在訊號處理函式中調⽤wait清理⼦程序即可。

請編寫⼀個程式完成以下功能:⽗程序fork出⼦程序,⼦程序調⽤exit(2)終⽌,⽗程序⾃定 義SIGCHLD訊號的處理函式,在其中調⽤wait獲得⼦程序的退出狀態並列印。

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void handler(int sig)
{
 pid_t id;
 while( (id = waitpid(-1, NULL, WNOHANG)) > 0){
 printf("wait child success: %d\n", id);
 }
 printf("child is quit! %d\n", getpid());
}
int main()
{
 signal(SIGCHLD, handler);
 pid_t cid;
 if((cid = fork()) == 0){//child
 printf("child : %d\n", getpid());
 sleep(3);
 exit(1);
 }
 while(1){
 printf("father proc is doing some thing!\n");
 sleep(1);
 }
 return 0;
}