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,不記錄該信號產⽣了多少次,阻塞標誌也是這樣表⽰的。 因此,未決和阻塞標誌可以⽤相同的數據類型sigset_t來存儲,sigset_t稱爲信號集,這個類型可以表⽰每個信號的“有效”或“⽆效”狀態,在阻塞信號集中“有效”和“⽆效”的含義是該信號是否被阻塞,⽽在未決信

號集中“有效”和“⽆效”的含義是該信號是否處於未決狀態。 阻塞信號集也叫做當前進程的信號屏蔽字(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參數的可選值。

SIG_BLOCK:set包含了我們希望添加到當前信號屏蔽字的信號,相當於mask=mask|set
SIG_UNBLOCK:set包含了我們希望從當前信號屏蔽字中解除阻塞的信號相當於mask=mask&~set
SIG_SETMASK:設置當前信號屏蔽字爲set所指向的值,相當於mask=set

如果調用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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章