第十一章 進程和信號(二)

信號 

      信號是UNIX與Linux系統響應某些條件而產生的一個事件。接收到該信號的進程會相應地採取一些行動。我們用術語(raise)表示一個信號的產生,使用術語(catch)來表示接收到一個信號。信號是由於某些錯誤條件而生成的,如內存段衝突、浮點處理器錯誤或非法指令。他們由 shell 和終端處理器生成來引起中斷,它們還可以作爲在進程間傳遞消息或修改行爲的一種方式,明確地由一個進程發送給另一個進程。無論何種情況,它們的編程接口都是相同的。信號可以被生成、捕獲、響應或忽略。

      信號的名字是在頭文件 signal.h 中定義的。他們以 "SIG" 開頭,見表:

信號名稱 說明
SIGABORT
*進程異常終止
SIGALRM
超時警告
SIGFPE
*浮點運算異常
SIGHUP
連接掛斷
SIGILL 
*非法指令
SIGINT
終端中斷
SIGKILL
終止進程(此信號不能被捕獲或忽略)
SIGPIPE
向無讀進程的管道寫數據
SIGQUIT
終端退出
SIGSEGV
*無效內存段訪問
SIGTERM
終止
SIGUSR1 
用戶定義信號1
SIGUSR2 
用戶定義信號2
             *  系統對信號的響應視具體實現而定。

         如果進程接收到這些信號中的一個,但事先沒有安排捕獲它,進程將會立刻終止。通常,系統將生成核心轉儲文件 core ,並將其放在當前目錄下。該文件是進程在內存中的映像,它對程序的調試很有用處。

           其它信號見表

信號名稱 說明
SIGCHLD
子進程已經停止或退出
SIGCONT 
繼續執行暫停進程
SIGSTOP
停止執行(此信號不能被捕獲或忽略)
SIGTSTP
終端掛起
SIGTTIN
後臺進程嘗試讀操作
SIGTTOU
後臺進程嘗試寫操作

        SIGCHLD 信號對於管理子進程很有用。默認情況下,它是被忽略的。其餘的信號會使接收它們的進程停止運行,但 SIGCONT 是個另外,它的作用是讓進程恢復並繼續執行。shell 腳本通過它來控製作業,但用戶程序很少會用到它。

        現在,我們只需要知道如果 shell 和終端驅動程序是按通常情況配置的話,在鍵盤上敲入中斷字符(通常是Ctrl+C組合鍵)就會向前臺進程(即當前正在運行的程序)發送 SIGINT 信號,這將引起該程序的終止,除非它事先安排了捕獲這個信號。

        如果想發送一個信號給進程,而該進程並不是當前的前臺進程,就需要用 kill 命令。該命令需要有一個可選的信號代碼或信號名稱和一個接收信號的目標進程的PID(這個PID一般需要用 ps 命令查出來)。例如,如果要向運行在另一個終端上的 PID 爲512的進程發送 “掛斷” 信號,可以使用如下命令:

$ kill -HUP 512

       kill 命令有一個有用的變體叫 killall,它可以給運行着某一命令的所有進程發送信號。並不是所有的Unix系統都支持它,但 Linux 系統一般都有該命令。如果不知道某個進程的 PID,或者想給執行相同命令的許多不同的進程發送信號,這條命令就很有用了。一種常見的用法是,通知 inetd 程序重新讀取它的配置選項,要完成這一工作,可以使用下面這條命令:

$ killall -HUP inetd

程序可以用 signal 庫函數來處理信號,它的定義如下:
#include <signal.h>
void ( *signal ( int sig, void ( *func ) ( int ) ) ) ( int );

       這個相當複雜的函數定義說明,signal 是一個帶有 sig 和 func 兩個參數的函數。準備捕獲或忽略的信號由參數 sig 給出,接收到指定的信號後將要調用的函數由參數 func 給出。信號處理函數必須有一個 int 類型參數(即接收到的信號代碼)並且返回類型爲 void 。signal 函數本身也返回一個同類型的函數,即先前用來處理這個信號的函數,或者也可以用下表中的兩個特殊值之一來代替信號處理函數。

SIG_IGN 忽略信號
SIG_DFL 恢復默認行爲
示例:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
 
void ouch(int sig)
{
    printf("OUCH! - I got signal %d\n", sig);
    (void) signal(SIGINT, SIG_DFL);
}

int main()
{
    (void) signal(SIGINT, ouch);
    
    while(1)
    {
        printf("Hello World\n");
	sleep(1);
    }
}

       main函數的作用是,截獲按下 Ctrl+C 組合鍵時產生的 SIGINT 信號。沒i有信號出現時,它會在一個無限循環中每隔一秒打印一條消息。

       第一次按入 Ctrl+C 組合鍵會讓程序做出響應,然後程序繼續執行。再次按下 Ctrl+C 組合鍵時,程序結束運行,因爲 SIGINT 信號的處理方式已恢復爲默認行爲 --- 終止程序的運行。

$ ./ctrlc1
Hello World!
Hello World!
Hello World!
Hello World!
^C
OUCH! - I got signal 2
Hello World!
Hello World!
Hello World!
Hello World!
^C
$

       在此例中我們可以看到,信號處理函數使用了一個單獨的整數參數,它就是引起該函數被調用的信號代碼。如果需要在同一個函數中處理多個信號,這個參數就很有用。在本例中,我們打印出 SIGINT 的值,它的值在這個系統中恰好是 2 ,但你不能過分依賴傳統的信號數字值,而應該在新的程序中總是使用信號的名字。

實驗解析

       程序中安排函數 ouch 來處理在按下 Ctrl+C 組合鍵時所產生的 SIGINT 信號。程序會在中斷函數 ouch 處理完畢後繼續執行,但信號處理方式已經恢復爲默認行爲(不同版本的 UNIX 系統,特別是從 Berkley UNIX 衍生出來的那些版本,在對信號的處理方式上從歷史上就有些細微的不同。如果想讓信號的處理方式在信號發生後恢復到i其默認行爲,最好的方法就是自己寫出具體的信號處理代碼)。當它接收到第二個 SIGINT 信號後,程序將採取默認的行動,即終止程序的運行。
       如果想保留信號處理函數,讓它繼續響應用戶的 Ctrl+C 組合鍵,我們就需要再次調用 signal 函數來重新建立它。這會使信號在一段時間內無法得到處理,這段時間從調用中斷函數開始,到信號處理函數的重建爲止。如果在這段時間內程序接收到第二個信號,它就會違揹我們的意願終止程序的運行。
      注:我們不推薦大家使用 signal 接口。之所以會在這裏介紹它,是因爲你可能會在許多老程序中看到它的應用。稍後我們會介紹一個定義更清晰、執行更可靠的函數 sigaction ,在所有的新程序中都應該使用這個函數。
      signal 函數返回的是先前對指定信號進行處理的信號處理函數的函數指針,如果未定義信號處理函數,則返回 SIG_ERR並設置 errno 爲一個正數值。如果輸出的是一個無效的信號,或者嘗試處理的信號是不可捕獲或不可忽略的信號(如SIGKILL),errno將被設置爲 EINVAL。


發送信號 
       進程可以通過調用 kill 函數向包括它本身在內的其它進程發送一個信號。如果程序沒有發送該信號的權限,對 kill 函數的調用就將失敗,失敗的常見原因是目標進程由另一個用戶所擁有。這個函數和同名的 shell 命令完成相同的功能,它的定義如下:
#include <sys/types.h>
#include <signal.h>
int kill( pid_t pid, int sig );
       kill 函數把參數 sig 給定的信號發送給由參數 pid 給出的進程號所指定的進程,成功時它返回 0 。要想發送一個信號,發送進程必須擁有相應的權限。這通常意味着兩個進程必須擁有相同的用戶 ID (即你只能發送信號給屬於自己的進程,但超級用戶可以發送信號給任何進程)。
       kill 調用會在失敗時返回 -1 並設置 errno 變量。失敗的原因可能是:給定的信號無效(errno設置爲EINVAL);發送進程權限不夠(errno 設置爲 EPERM);目標進程(errno 設置爲 ESRCH)。
       信號爲我們提供了一個有用的鬧鐘功能。進程可以通過調用 alarm 函數在經過預定時間後發送一個 SIGALRM 信號。

#include <unistd.h>
unsigned int alarm ( unsigned int seconds );
      alarm 函數用來在 seconds 秒之後安排發送一個 SIGALARM 信號。但由於處理的延時和時間調度的不確定性,實際鬧鐘時間將比預先安排的要稍微拖後一點。把參數 seconds 設置爲 0 將取消所有已設置的鬧鐘請求。如果在接收到 SIGALARM 信號之前再次調用 alarm 函數,則鬧鐘開始重新計時。每個進程只能有一個鬧鐘時間。alarm 函數的返回值是以前設置的鬧鐘時間的餘留秒數,如果調用失敗則返回 -1.。
      爲了說明 alarm 函數的工作情況,我們通過使用 fork,sleep 和 signal 來模擬它的效果。程序可以啓動一個新的進程,它專門用於在未來的某一時刻發送一個信號。

實驗  模擬一個鬧鐘

        alarm.c程序裏的第一個函數 ding 的作用是模擬一個鬧鐘。

        在main函數中,我們告訴子進程在等待 5 秒後發送一個 SIGALRM 信號給它的父進程。

        父進程通過一個 signal 調用安排好捕獲 SIGALRM 信號的工作,然後等待它的到來。

/*  In alarm.c, the first function, ding, simulates an alarm clock.  */

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

static int alarm_fired = 0;

void ding(int sig)
{
    alarm_fired = 1;
}

/*  In main, we tell the child process to wait for five seconds
    before sending a SIGALRM signal to its parent.  */

int main()
{
    pid_t pid;

    printf("alarm application starting\n");

    pid = fork();
    switch(pid) {
    case -1:
      /* Failure */
      perror("fork failed");
      exit(1);
    case 0:
      /* child */
        sleep(5);
        kill(getppid(), SIGALRM);
        exit(0);
    }

/*  The parent process arranges to catch SIGALRM with a call to signal
    and then waits for the inevitable.  */

    printf("waiting for alarm to go off\n");
    (void) signal(SIGALRM, ding);

    pause();
    if (alarm_fired)
        printf("Ding!\n");

    printf("done\n");
    exit(0);
}

        運行這個程序時,它會暫停 5 秒,等待模擬鬧鐘的鬧響。

$ ./alarm
alarm application starting
waiting for alarm to go off
<5 second pause>
Ding!
done
$

        這個程序用到了一個新的函數 pause,它的作用很簡單,就是把程序的執行掛起直到有一個信號出現爲止。當程序接收到一個信號時,預先設定好的信號處理函數開始運行,程序也將恢復正常的執行。pause 函數的定義如下所示:
#include <unistd.h>
int pause(void);
        當它被一個信號中斷時,將返回 -1 (如果下一個接收到的信號沒有導致程序終止的話)並把 errno 設置爲 EINTR。當需要等待信號時,一個更常見的方法是使用稍後將要介紹的 sigaction 函數。

實驗解析

       鬧鐘模擬程序通過 fork 調用啓動新的程序。這個子進程休眠 5 秒後向其父進程發送一個 SIGALRM 信號。父進程在安排好捕獲 SIGALRM 信號後暫停運行,直到接收到一個信號爲止。我們並未在信號處理函數中直接調用 printf ,而是通過在該函數中設置標誌,然後在 main 函數中檢查該標誌來完成消息的輸出。
       使用信號並掛起執行是 Linux 程序設計的一個重要部分。這意味着程序不需要總是在執行着。程序不必在一個循環中無休止地檢查某個事件是否已經發生,相反,它可以等待事件的發生。這在只有一個 CPU 的多用戶環境中尤其重要,進程共享着一個處理器,繁忙的等待將會對系統的性能造成極大的影響。程序中信號的使用將帶來一個特殊問題:“如果信號出現在系統調用的執行過程中會發生什麼情況?”答案是相當讓人不滿意的“視情況而定”。一般來說。你只需要考慮系統調用,例如從終端讀取數據,如果在這個系統調用等待數據時出現一個信號,它就會返回一個錯誤。如果你開始在自己的程序中使用信號,就需要注意一些系統調用會因爲接收到了一個信號而失敗,而這種錯誤情況可能是你在添加信號處理函數之前沒有考慮到的。
        在編寫程序中信號處理部分的代碼時必須非常的小心,因爲在使用信號的程序中會出現各種各樣的"競態條件"。例如,如果想調用 pause 等待一個信號,可信號出現在 pause 之前,就會使程序無限期地等待一個不會發生的事件。這些競態條件都是一些對時間要求很苛刻的問題,許多編程新手都有這方面的煩惱,所以在檢查和信號相關的代碼時總是要非常小心。

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