Linux網絡編程:信號

一、爲了理解信號,先從我們最熟悉的場景說起:
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)中都有詳細說明。

0~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. 忽略此信號。有兩個信號不能被忽略:SIGKILL和SIGSTOP。
2. 執行該信號的默認處理動作。
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    恢復默認行爲

RETURN VALUE
       signal() returns the previous value of the signal handler, or SIG_ERR on error.

示例程序:
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<signal.h>

#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

void handler(int sig);

int main(int argc, char *argv[])
{
    __sighandler_t oldhandler;
    oldhandler = signal(SIGINT, handler); // 返回值是先前的信號處理程序函數指針
    if (oldhandler == SIG_ERR)
        ERR_EXIT("signal error");

    while (getchar() != '\n') ;

    /* signal(SIGINT, SIGDFL) */
    if (signal(SIGINT, oldhandler) == SIG_ERR)
        ERR_EXIT("signal error");
    for (; ;) ;

    return 0;
}

void handler(int sig)
{
    printf("recv a sig=%d\n", sig);
}
輸出如下所示:
^Crecv a sig=2
^Crecv a sig=2
^Crecv a sig=2
^C
程序執行開始註冊了SIGINT信號的處理函數,故我們按下ctrl+c 並不會像往常一樣終止程序,只是打印了recv a  sig = 2。接着按下回車,重新註冊了SIGINT的默認處理,此時再ctrl+c 程序就被終止了。
將程序中的 32 ~37 行 換成如下的表述:
for (; ;)
{
    pause(); //使進程掛起直到一個信號被捕獲(信號處理函數完成後返回)
    //且調用schedule()使系統調度其他程序運行,
    //這樣比完全的死循環的好處是讓出cpu
    printf("pause return\n");
}
調用pause函數:將進程置爲可中斷睡眠狀態。然後它調用schedule(),使linux進程調度器找到另一個進程來運行。pause使調用者進程掛起,直到一個信號被捕獲處理後函數才返回。調用pause 的好處是在等待信號的時候讓出cpu,讓系統調度其他進程運行,而不是完全的死循環,當然這樣ctrl+c 就是始終終止不了程序,我們可以使用 ctrl+\ 產生SIGQUIT信號終止程序。

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




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章