天天看點

UNIX環境進階程式設計——信号基本概述和signal函數

一、為了了解信号,先從我們最熟悉的場景說起:

1. 使用者輸入指令,在Shell下啟動一個前台程序。

2. 使用者按下Ctrl-C,這個鍵盤輸入産生一個硬體中斷。

3. 如果CPU目前正在執行這個程序的代碼,則該程序的使用者空間代碼暫停執行,CPU從使用者态切換到核心态處理硬體中斷。

4. 終端驅動程式将Ctrl-C解釋成一個SIGINT信号,記在該程序的PCB中(也可以說發送了一個SIGINT信号給該程序)。

5. 當某個時刻要從核心傳回到該程序的使用者空間代碼繼續執行之前,首先處理PCB中記錄的信号,發現有一個SIGINT信号待處理,而這個信号的預設處理動作是終止程序,是以直接終止程序而不再傳回它的使用者空間代碼執行。

用kill -l指令可以察看系統定義的信号清單:

每個信号都有一個編号和一個宏定義名稱,這些宏定義可以在signal.h中找到,例如其中有定義#define SIGINT 2。編号34以上的是實時信号,這些信号各自在什麼條件下産生,預設的處理動作是什麼(Term表示終止目前程序,Core表示終止目前程序并且Core Dump,Ign表示忽略該信号,Stop表示停止目前程序,Cont表示繼續執行先前停止的程序),在signal(7)中都有詳細說明。

[1,31]       不可靠信号,多個信号不會排隊隻保留一個,即信号可能丢失。

[34,64]    可靠(實時信号),支援排隊信号不會丢失,可使用sigqueue發送信号,不像0~31有預設的定義。

二、産生信号的條件主要有:

1、使用者在終端按下某些鍵時,終端驅動程式會發送信号給前台程序,例如Ctrl-C産生SIGINT信号,Ctrl-\産生SIGQUIT信号,Ctrl-Z産生SIGTSTP信号。

2、硬體異常産生信号,這些條件由硬體檢測到并通知核心,然後核心向目前程序發送适當的信号。例如目前程序執行了除以0的指令,CPU的運算單元會産生異常,核心将這個異常解釋為SIGFPE信号發送給程序。

3、再比如目前程序通路了非法記憶體位址,MMU會産生異常,核心将這個異常解釋為SIGSEGV信号發送給程序。

4、一個程序調用kill(2)函數可以發送信号給另一個程序。

5、可以用kill(1)指令發送信号給某個程序,kill(1)指令也是調用kill(2)函數實作的,如果不明确指定信号則發送SIGTERM信号,該信号的預設處理動作是終止程序。

6、raise:給自己發送信号。raise(sig)等價于kill(getpid(), sig);

7、killpg:給程序組發送信号。killpg(pgrp, sig)等價于kill(-pgrp, sig);

8、sigqueue:給程序發送信号,支援排隊,可以附帶資訊。

9、當核心檢測到某種軟體條件發生時也可以通過信号通知程序,例如鬧鐘逾時産生SIGALRM信号,向讀端已關閉的管道寫資料時産生SIGPIPE信号。

三、使用者程式可以調用signal(2) / sigaction(2)函數告訴核心如何處理某種信号(若未注冊則按預設處理),可選的處理動作有三種:

1. 忽略此信号(SIG_IGN)。有兩個信号不能被忽略:SIGKILL和SIGSTOP。

2. 執行該信号的預設處理動作(SIG_DFL)。

3. 提供一個信号處理函數,要求核心在處理該信号時切換到使用者态執行這個處理函數,這種方式稱為捕捉(catch)一個信号。

四、信号與中斷的差別

信号與中斷的相似點:

(1)采用了相同的異步通信方式;

(2)當檢測出有信号或中斷請求時,都暫停正在執行的程式而轉去執行相應的處理程式;

(3)都在處理完畢後傳回到原來的斷點;

(4)對信号或中斷都可進行屏蔽。

信号與中斷的差別:

(1)中斷有優先級,而信号沒有優先級,所有的信号都是平等的;

(2)信号處理程式是在使用者态下運作的,而中斷處理程式是在核心态下運作;

(3)中斷響應是及時的,而信号響應通常都有較大的時間延遲。

五、signal(2) 信号注冊函數

typedef void (*__sighandler_t) (int);

#define SIG_ERR ((__sighandler_t) -1)

#define SIG_DFL ((__sighandler_t) 0)

#define SIG_IGN ((__sighandler_t) 1)

函數原型:

__sighandler_t signal(int signum, __sighandler_t handler);

參數

signal是一個帶signum和handler兩個參數的函數,準備捕捉或屏蔽的信号由參數signum給出,接收到指定信号時将要調用的函數由handler給出,handler這個函數必須有一個int類型的參數(即接收到的信号代碼),它本身的類型是void

handler也可以是兩個特殊值:SIG_IGN 屏蔽該信号;SIG_DFL    恢複預設行為。

傳回值:傳回先前的信号處理函數指針(注意不是handler),如果有錯誤則傳回SIG_ERR(-1)。

測試輸出如下:

    程式執行開始注冊了SIGINT信号的處理函數,故我們按下ctrl+c 并不會像往常一樣終止程式,隻是列印了recv a  sig = 2。接着按下回車,重新注冊了SIGINT的預設處理,此時再ctrl+c 程式就被終止了。

将程式中的 29 ~38 行 換成如下的表述:

     調用pause函數:将程序置為可中斷睡眠狀态。然後它調用schedule(),使linux程序排程器找到另一個程序來運作。pause使調用者程序挂起,直到一個信号被捕獲處理後函數才傳回。調用pause 的好處是在等待信号的時候讓出cpu,讓系統排程其他程序運作,而不是完全的死循環,當然這樣ctrl+c 就是始終終止不了程式,我們可以使用 ctrl+\ 産生SIGQUIT信号終止程式。

     事實上根據man手冊,signal 函數可移植性并不是很好,最好隻是用在SIG_DFL, SIG_IGN 上,注冊信号處理函數用sigaction 比較好。

注意:當一個程序調用fork時,其子程序繼承父程序的信号處理方式。