APUE第10章 信號(hào)

第10章 信號(hào)

signal

10.1 引言

信號(hào)是軟件中斷。很多比較重要的應(yīng)用程序都需處理信號(hào)。信號(hào)提 供了一種處理異步事件的方法,例如,終端用戶鍵入中斷鍵,會(huì)通過信 號(hào)機(jī)制停止一個(gè)程序,或及早終止管道中的下一個(gè)程序。

UNIX系統(tǒng)的早期版本就已經(jīng)提供信號(hào)機(jī)制,但是這些系統(tǒng)(如 V7)所提供的信號(hào)模型并不可靠。信號(hào)可能丟失,而且在執(zhí)行臨界區(qū)代 碼時(shí),進(jìn)程很難關(guān)閉所選擇的信號(hào)。4.3BSD 和 SVR3對(duì)信號(hào)模型都做了 更改,增加了可靠信號(hào)機(jī)制。但是Berkeley和AT&T所做的更改之間并不 兼容。幸運(yùn)的是,POSIX.1對(duì)可靠信號(hào)例程進(jìn)行了標(biāo)準(zhǔn)化,這正是本章 所要說明的。

本章先對(duì)信號(hào)機(jī)制進(jìn)行綜述,并說明每種信號(hào)的一般用法。然后分 析早期實(shí)現(xiàn)的問題。在分析存在的問題之后再說明解決這些問題的方 法,這種安排有助于加深對(duì)改進(jìn)機(jī)制的理解。本章也包含了很多并非完 全正確的實(shí)例,這樣做的目的是為了對(duì)其不足之處進(jìn)行討論。

10.2 信號(hào)概念

首先,每個(gè)信號(hào)都有一個(gè)名字。這些名字都以3個(gè)字符SIG開頭。例 如,SIGABRT是夭折信號(hào),當(dāng)進(jìn)程調(diào)用abort函數(shù)時(shí)產(chǎn)生這種信號(hào)。 SIGALRM是鬧鐘信號(hào),由alarm函數(shù)設(shè)置的定時(shí)器超時(shí)后將產(chǎn)生此信 號(hào)。V7 有 15 種不同的信號(hào),SVR4 和 4.4BSD 均有 31 種不同的信號(hào)。 FreeBSD 8.0支持32種信號(hào),Mac OS X 10.6.8以及Linux 3.2.0都支持31種信 號(hào),而Solaris 10支持40種信號(hào)。但是,F(xiàn)reeBSD、Linux和Solaris作為實(shí)時(shí) 擴(kuò)展都支持另外的應(yīng)用程序定義的信號(hào)。雖然本書不包括POSIX實(shí)時(shí)擴(kuò) 展(有關(guān)信息請(qǐng)參閱Gallmeister[1995]),但是SUSv4已經(jīng)把實(shí)時(shí)信號(hào)接 口移至基礎(chǔ)規(guī)范說明中。

在頭文件<signal.h>中,信號(hào)名都被定義為正整數(shù)常量(信號(hào)編 號(hào))。
實(shí)際上,實(shí)現(xiàn)將各信號(hào)定義在另一個(gè)頭文件中,但是該頭文件又包 括在<signal.h>中。內(nèi)核包括對(duì)用戶級(jí)應(yīng)用程序有意義的頭文件,這被 認(rèn)為是一種不好的形式,所以如若應(yīng)用程序和內(nèi)核兩者都需使用同一定 義,那么就將有關(guān)信息放置在內(nèi)核頭文件中,然后用戶級(jí)頭文件再包括 該內(nèi)核頭文件。于是,F(xiàn)reeBSD 8.0和Mac OS X 10.6.8將信號(hào)定義在 <sys/signal.h>中,Linux 3.2.0將信號(hào)定義在<bits/signum.h>中, Solaris 10將信號(hào)定義在<sys/iso/signal_iso.h>中。

不存在編號(hào)為 0 的信號(hào)。在 10.9 節(jié)中將會(huì)看到,kill 函數(shù)對(duì)信號(hào)編 號(hào) 0 有特殊的應(yīng)用。POSIX.1將此種信號(hào)編號(hào)值稱為空信號(hào)。
很多條件可以產(chǎn)生信號(hào)。 ?當(dāng)用戶按某些終端鍵時(shí),引發(fā)終端產(chǎn)生的信號(hào)。在終端上按 Delete

鍵(或者很多系統(tǒng)中的Ctrl+C鍵)通常產(chǎn)生中斷信號(hào)(SIGINT)。這是 停止一個(gè)已失去控制程序的方法。(第18章將說明此信號(hào)可被映射為終 端上的任一字符。)

?硬件異常產(chǎn)生信號(hào):除數(shù)為0、無效的內(nèi)存引用等。這些條件通常 由硬件檢測(cè)到,并通知內(nèi)核。然后內(nèi)核為該條件發(fā)生時(shí)正在運(yùn)行的進(jìn)程 產(chǎn)生適當(dāng)?shù)男盘?hào)。例如,對(duì)執(zhí)行一個(gè)無效內(nèi)存引用的進(jìn)程產(chǎn)生SIGSEGV 信號(hào)。
?進(jìn)程調(diào)用kill(2)函數(shù)可將任意信號(hào)發(fā)送給另一個(gè)進(jìn)程或進(jìn)程組。自 然,對(duì)此有所限制:接收信號(hào)進(jìn)程和發(fā)送信號(hào)進(jìn)程的所有者必須相同, 或發(fā)送信號(hào)進(jìn)程的所有者必須是超級(jí)用戶。

?用戶可用kill(1)命令將信號(hào)發(fā)送給其他進(jìn)程。此命令只是kill函數(shù)的 接口。常用此命令終止一個(gè)失控的后臺(tái)進(jìn)程。
?當(dāng)檢測(cè)到某種軟件條件已經(jīng)發(fā)生,并應(yīng)將其通知有關(guān)進(jìn)程時(shí)也產(chǎn) 生信號(hào)。這里指的不是硬件產(chǎn)生條件(如除以 0),而是軟件條件。例 如 SIGURG(在網(wǎng)絡(luò)連接上傳來帶外的數(shù)據(jù))、SIGPIPE(在管道的讀 進(jìn)程已終止后,一個(gè)進(jìn)程寫此管道)以及 SIGALRM(進(jìn)程所設(shè)置的定 時(shí)器已經(jīng)超時(shí))。
信號(hào)是異步事件的經(jīng)典實(shí)例。產(chǎn)生信號(hào)的事件對(duì)進(jìn)程而言是隨機(jī)出 現(xiàn)的。進(jìn)程不能簡單地測(cè)試一個(gè)變量(如errno)來判斷是否發(fā)生了一個(gè) 信號(hào),而是必須告訴內(nèi)核“在此信號(hào)發(fā)生時(shí),請(qǐng)執(zhí)行下列操作”。
在某個(gè)信號(hào)出現(xiàn)時(shí),可以告訴內(nèi)核按下列3種方式之一進(jìn)行處理, 我們稱之為信號(hào)的處理或與信號(hào)相關(guān)的動(dòng)作。

(2)捕捉信號(hào)。為了做到這一點(diǎn),要通知內(nèi)核在某種信號(hào)發(fā)生 時(shí),調(diào)用一個(gè)用戶函數(shù)。在用戶函數(shù)中,可執(zhí)行用戶希望對(duì)這種事件進(jìn) 行的處理。例如,若正在編寫一個(gè)命令解釋器,它將用戶的輸入解釋為 命令并執(zhí)行之,當(dāng)用戶用鍵盤產(chǎn)生中斷信號(hào)時(shí),很可能希望該命令解釋 器返回到主循環(huán),終止正在為該用戶執(zhí)行的命令。如果捕捉到 SIGCHLD 信號(hào),則表示一個(gè)子進(jìn)程已經(jīng)終止,所以此信號(hào)的捕捉函數(shù) 可以調(diào)用waitpid以取得該子進(jìn)程的進(jìn)程ID以及它的終止?fàn)顟B(tài)。又例如, 如果進(jìn)程創(chuàng)建了臨時(shí)文件,那么可能要為 SIGTERM 信號(hào)編寫一個(gè)信號(hào) 捕捉函數(shù)以清除臨時(shí)文件(SIGTERM 是終止信號(hào),kill 命令傳送的系統(tǒng) 默認(rèn)信號(hào)是終止信號(hào))。注意,不能捕捉SIGKILL和SIGSTOP信號(hào)。

SIGABRT 調(diào)用abort函數(shù)時(shí)(見10.17節(jié))產(chǎn)生此信號(hào)。進(jìn)程異常終 止。
SIGALRM 當(dāng)用alarm函數(shù)設(shè)置的定時(shí)器超時(shí)時(shí),產(chǎn)生此信號(hào)。詳細(xì) 情況見10.10節(jié)。若由setitimer(2)函數(shù)設(shè)置的間隔時(shí)間已經(jīng)超時(shí)時(shí),也產(chǎn) 生此信號(hào)。
SIGBUS 指示一個(gè)實(shí)現(xiàn)定義的硬件故障。當(dāng)出現(xiàn)某些類型的內(nèi)存故
障時(shí)(如 14.8 節(jié)中說明的),實(shí)現(xiàn)常常產(chǎn)生此種信號(hào)。
SIGCANCEL 這是Solaris線程庫內(nèi)部使用的信號(hào)。它不適用于一般
應(yīng)用。
SIGCHLD 在一個(gè)進(jìn)程終止或停止時(shí),SIGCHLD信號(hào)被送給其父進(jìn)
程。按系統(tǒng)默認(rèn),將忽略此信號(hào)。如果父進(jìn)程希望被告知其子進(jìn)程的這 種狀態(tài)改變,則應(yīng)捕捉此信號(hào)。信號(hào)捕捉函數(shù)中通常要調(diào)用一種wait函 數(shù)以取得子進(jìn)程ID和其終止?fàn)顟B(tài)。System V的早期版本有一個(gè)名為 SIGCLD(無H)的類似信號(hào)。這一信號(hào)具有與其他信號(hào)不同的語義, SVR2的手冊(cè)頁警告在新的程序中盡量不要使用這種信號(hào)。(令人奇怪 的是,在SVR3和SVR4版的手冊(cè)頁中,該警告消失了。)應(yīng)用程序應(yīng)當(dāng) 使用標(biāo)準(zhǔn)的SIGCHLD信號(hào),但應(yīng)了解,為了向后兼容,很多系統(tǒng)定義 了與SIGCHLD等同的SIGCLD。如果有使用SIGCLD的軟件,需要查閱 系統(tǒng)手冊(cè),了解它具體的語義。10.7節(jié)將討論這兩個(gè)信號(hào)。
SIGCONT 此作業(yè)控制信號(hào)發(fā)送給需要繼續(xù)運(yùn)行,但當(dāng)前處于停止 狀態(tài)的進(jìn)程。如果接收到此信號(hào)的進(jìn)程處于停止?fàn)顟B(tài),則系統(tǒng)默認(rèn)動(dòng)作 是使該進(jìn)程繼續(xù)運(yùn)行;否則默認(rèn)動(dòng)作是忽略此信號(hào)。例如,全屏編輯程 序在捕捉到此信號(hào)后,使用信號(hào)處理程序發(fā)出重新繪制終端屏幕的通 知。關(guān)于進(jìn)一步的情況見10.21節(jié)。
SIGEMT 指示一個(gè)實(shí)現(xiàn)定義的硬件故障。

SIGFPE 此信號(hào)表示一個(gè)算術(shù)運(yùn)算異常,如除以0、浮點(diǎn)溢出等。
SIGFREEZE 此信號(hào)僅由Solaris定義。它用于通知進(jìn)程在凍結(jié)系統(tǒng) 狀態(tài)之前需要采取特定動(dòng)作,例如當(dāng)系統(tǒng)進(jìn)入休眠或掛起狀態(tài)時(shí)可能需 要做這種處理。
SIGHUP 如果終端接口檢測(cè)到一個(gè)連接斷開,則將此信號(hào)送給與該
終端相關(guān)的控制進(jìn)程(會(huì)話首進(jìn)程)。見圖9-13,此信號(hào)被送給session 結(jié)構(gòu)中s_leader字段所指向的進(jìn)程。僅當(dāng)終端的CLOCAL標(biāo)志沒有設(shè)置 時(shí),在上述條件下才產(chǎn)生此信號(hào)。(如果所連接的終端是本地的,則設(shè) 置該終端的CLOCAL標(biāo)志。它告訴終端驅(qū)動(dòng)程序忽略所有調(diào)制解調(diào)器的 狀態(tài)行。第18章將說明如何設(shè)置此標(biāo)志。)
SIGILL 此信號(hào)表示進(jìn)程已執(zhí)行一條非法硬件指令。
SIGINFO 這是一種BSD信號(hào),當(dāng)用戶按狀態(tài)鍵(一般采用Ctrl+T) 時(shí),終端驅(qū)動(dòng)程序產(chǎn)生此信號(hào)并發(fā)送至前臺(tái)進(jìn)程組中的每一個(gè)進(jìn)程(見 圖 9-9)。此信號(hào)通常造成在終端上顯示前臺(tái)進(jìn)程組中各進(jìn)程的狀態(tài)信 息。

SIGINT 當(dāng)用戶按中斷鍵(一般采用 Delete 或 Ctrl+C)時(shí),終端驅(qū) 動(dòng)程序產(chǎn)生此信號(hào)并發(fā)送至前臺(tái)進(jìn)程組中的每一個(gè)進(jìn)程(見圖9-9)。 當(dāng)一個(gè)進(jìn)程在運(yùn)行時(shí)失控,特別是它正在屏幕上產(chǎn)生大量不需要的輸出 時(shí),常用此信號(hào)終止它。

SIGKILL 這是兩個(gè)不能被捕捉或忽略信號(hào)中的一個(gè)。它向系統(tǒng)管理
員提供了一種可以殺死任一進(jìn)程的可靠方法。

SIGQUIT 當(dāng)用戶在終端上按退出鍵(一般采用Ctrl+)時(shí),中斷驅(qū) 動(dòng)程序產(chǎn)生此信號(hào),并發(fā)送給前臺(tái)進(jìn)程組中的所有進(jìn)程(見圖9-9)。 此信號(hào)不僅終止前臺(tái)進(jìn)程組(如SIGINT所做的那樣),同時(shí)產(chǎn)生一個(gè) core文件。
SIGSEGV 指示進(jìn)程進(jìn)行了一次無效的內(nèi)存引用(通常說明程序有 錯(cuò),比如訪問了一個(gè)未經(jīng)初始化的指針)。

SIGSTOP 這是一個(gè)作業(yè)控制信號(hào),它停止一個(gè)進(jìn)程。它類似于交 互停止信號(hào)(SIGTSTP),但是SIGSTOP不能被捕捉或忽略。

SIGKILL 和SIGSTOP 不能被忽略和捕捉

SIGTERM 這是由kill(1)命令發(fā)送的系統(tǒng)默認(rèn)終止信號(hào)。由于該信號(hào) 是由應(yīng)用程序捕獲的,使用SIGTERM也讓程序有機(jī)會(huì)在退出之前做好 清理工作,從而優(yōu)雅地終止(相對(duì)于SIGKILL而言。SIGKILL不能被捕 捉或者忽略)。

10.3 signal函數(shù)

函數(shù)signal

UNIX系統(tǒng)信號(hào)機(jī)制最簡單的接口是signal函數(shù)。

#include <signal.h>
    void (*signal(int signo, void (*func)(int)))(int);

返回值:若成功,返回以前的信號(hào)處理配置;若出錯(cuò),返回SIG_ERR

signal函數(shù)由ISO C定義。因?yàn)镮SO C不涉及多進(jìn)程、進(jìn)程組以及終 端I/O等,所以它對(duì)信號(hào)的定義非常含糊,以致于對(duì)UNIX系統(tǒng)而言幾乎 毫無用處。
從UNIX System V派生的實(shí)現(xiàn)支持signal函數(shù),但該函數(shù)提供舊的 不可靠信號(hào)語義(10.4節(jié)將說明這些舊的語義)。提供此函數(shù)主要是為 了向后兼容要求此舊語義的應(yīng)用程序,新應(yīng)用程序不應(yīng)使用這些不可靠 信號(hào)。
4.4BSD 也提供 signal 函數(shù),但它是按照 sigaction 函數(shù)定義
所以在 4.4BSD 之下使用它提供 新的可靠信號(hào)語義。目前大多數(shù)系統(tǒng)遵循這種策略,但Solaris 10沿用 System V signal函數(shù)的語義。

signo參數(shù)是圖10-1中的信號(hào)名。func的值是常量SIG_IGN、常量 SIG_DFL或當(dāng)接到此信號(hào)后要調(diào)用的函數(shù)的地址。如果指定SIG_IGN, 則向內(nèi)核表示忽略此信號(hào)(記住有兩個(gè)信號(hào)SIGKILL和SIGSTOP不能忽略

當(dāng)指定函數(shù)地址時(shí),則在信號(hào)發(fā)生時(shí),調(diào) 用該函數(shù),我們稱這種處理為捕捉該信號(hào),稱此函數(shù)為信號(hào)處理程序 (signal handler)或信號(hào)捕捉函數(shù)(signal-catching function)。

#include <iostream>
#include <stdio.h>
#include <sys/signal.h>
#include <unistd.h>

void add(int x) {
    std::cout << "use signal slover" << std::endl;
    exit(0);
}

int main() {

    /*----------------------------------- test signal ----------------------------------------*/
    void (*f)(int x) = add;
    signal(SIGINT, f);

    for (int i = 0; i < 10; ++i) {
        std::cout << "hello" << std::endl;
        sleep(1);
    }

    return 0;
}

我們可以捕捉信號(hào)進(jìn)行處理,或者忽略信號(hào)
捕捉信號(hào)SIGTSTP

#include <iostream>
#include <stdio.h>
#include <sys/signal.h>
#include <unistd.h>

void add(int x) {
    std::cout << "use signal slover" << std::endl;
    std::cout << "catch SIGTSTP" << std::endl;
    exit(0);
}

int main() {

    /*----------------------------------- test signal ----------------------------------------*/
    void (*f)(int x) = add;
    signal(SIGTSTP, f);

    for (int i = 0; i < 10; ++i) {
        std::cout << "hello" << std::endl;
        sleep(1);
    }

    return 0;
}

運(yùn)行時(shí)像進(jìn)程kill -TSTP pid或者輸入ctrl + z即可捕捉到

或者忽略指定信號(hào)(SIGKILL 和SIGSTOP不能忽略)

10.5 中斷的系統(tǒng)調(diào)用

早期UNIX系統(tǒng)的一個(gè)特性是:如果進(jìn)程在執(zhí)行一個(gè)低速系統(tǒng)調(diào)用 而阻塞期間捕捉到一個(gè)信號(hào),則該系統(tǒng)調(diào)用就被中斷不再繼續(xù)執(zhí)行。該 系統(tǒng)調(diào)用返回出錯(cuò),其errno設(shè)置為EINTR。這樣處理是因?yàn)橐粋€(gè)信號(hào)發(fā) 生了,進(jìn)程捕捉到它,這意味著已經(jīng)發(fā)生了某種事情,所以是個(gè)好機(jī)會(huì) 應(yīng)當(dāng)喚醒阻塞的系統(tǒng)調(diào)用。
在這里,我們必須區(qū)分系統(tǒng)調(diào)用和函數(shù)。當(dāng)捕捉到某個(gè)信號(hào)時(shí),被 中斷的是內(nèi)核中執(zhí)行的系統(tǒng)調(diào)用。
為了支持這種特性,將系統(tǒng)調(diào)用分成兩類:低速系統(tǒng)調(diào)用和其他系 統(tǒng)調(diào)用。低速系統(tǒng)調(diào)用是可能會(huì)使進(jìn)程永遠(yuǎn)阻塞的一類系統(tǒng)調(diào)用,包 括:

?如果某些類型文件(如讀管道、終端設(shè)備和網(wǎng)絡(luò)設(shè)備)的數(shù)據(jù)不 存在,則讀操作可能會(huì)使調(diào)用者永遠(yuǎn)阻塞;
?如果這些數(shù)據(jù)不能被相同的類型文件立即接受,則寫操作可能會(huì) 使調(diào)用者永遠(yuǎn)阻塞;
?在某種條件發(fā)生之前打開某些類型文件,可能會(huì)發(fā)生阻塞(例如 要打開一個(gè)終端設(shè)備,需要先等待與之連接的調(diào)制解調(diào)器應(yīng)答);
?pause函數(shù)(按照定義,它使調(diào)用進(jìn)程休眠直至捕捉到一個(gè)信號(hào)) 和wait函數(shù);
?某些ioctl操作;
?某些進(jìn)程間通信函數(shù)

在這些低速系統(tǒng)調(diào)用中,一個(gè)值得注意的例外是與磁盤I/O有關(guān)的
系統(tǒng)調(diào)用。雖然讀、寫一個(gè)磁盤文件可能暫時(shí)阻塞調(diào)用者(在磁盤驅(qū)動(dòng)

程序?qū)⒄?qǐng)求排入隊(duì)列,然后在適當(dāng)時(shí)間執(zhí)行請(qǐng)求期間),但是除非發(fā)生 硬件錯(cuò)誤,I/O操作總會(huì)很快返回,并使調(diào)用者不再處于阻塞狀態(tài)。
可以用中斷系統(tǒng)調(diào)用這種方法來處理的一個(gè)例子是:一個(gè)進(jìn)程啟動(dòng) 了讀終端操作,而使用該終端設(shè)備的用戶卻離開該終端很長時(shí)間。在這 種情況下,進(jìn)程可能處于阻塞狀態(tài)幾個(gè)小時(shí)甚至數(shù)天,除非系統(tǒng)停機(jī), 否則一直如此。

對(duì)于中斷的read、write系統(tǒng)調(diào)用,POSIX.1的語義在該標(biāo)準(zhǔn)的2001 版有所改變。對(duì)于如何處理已 read、write 部分?jǐn)?shù)據(jù)量的相應(yīng)系統(tǒng)調(diào) 用,早期版本允許實(shí)現(xiàn)自行選擇。如若 read系統(tǒng)調(diào)用已接收并傳送數(shù) 據(jù)至應(yīng)用程序緩沖區(qū),但尚未接收到應(yīng)用程序請(qǐng)求的全部數(shù)據(jù),此時(shí)被 中斷,操作系統(tǒng)可以認(rèn)為該系統(tǒng)調(diào)用失敗,并將 errno 設(shè)置為 EINTR;另一種處理方式是允許該系統(tǒng)調(diào)用成功返回,返回值是已接收 到的數(shù)據(jù)量。與此類似,如若write巳傳輸了應(yīng)用程序緩沖區(qū)中的部分 數(shù)據(jù),然后被中斷,操作系統(tǒng)可以認(rèn)為該系統(tǒng)調(diào)用失敗,并將errno設(shè) 置為EINTR;另一種處理方式是允許該系統(tǒng)調(diào)用成功返回,返回值是已 寫部分的數(shù)據(jù)量。歷史上,從System V派生的實(shí)現(xiàn)將這種系統(tǒng)調(diào)用視為 失敗,而 BSD 派生的實(shí)現(xiàn)則處理為部分成功返回。2001 版 POSIX.1標(biāo) 準(zhǔn)采用BSD風(fēng)格的語義。

與被中斷的系統(tǒng)調(diào)用相關(guān)的問題是必須顯式地處理出錯(cuò)返回。典型 的代碼序列(假定進(jìn)行一個(gè)讀操作,它被中斷,我們希望重新啟動(dòng)它) 如下:

    again:
      if ((n = read(fd, buf, BUFFSIZE)) < 0) {
        if (errno == EINTR)
          goto again; /* just an interrupted system call */
        /* handle other errors */
      }

為了幫助應(yīng)用程序使其不必處理被中斷的系統(tǒng)調(diào)用,4.2BSD引進(jìn)了 某些被中斷系統(tǒng)調(diào)用的自動(dòng)重啟動(dòng)。自動(dòng)重啟動(dòng)的系統(tǒng)調(diào)用包括: ioctl、read、readv、write、writev、wait 和waitpid。如前所述,其中前5個(gè) 函數(shù)只有對(duì)低速設(shè)備進(jìn)行操作時(shí)才會(huì)被信號(hào)中斷。而wait和waitpid 在捕 捉到信號(hào)時(shí)總是被中斷。因?yàn)檫@種自動(dòng)重啟動(dòng)的處理方式也會(huì)帶來問 題,某些應(yīng)用程序并不希望這些函數(shù)被中斷后重啟動(dòng)。為此4.3BSD允許 進(jìn)程基于每個(gè)信號(hào)禁用此功能。
POSIX.1 要求只有中斷信號(hào)的SA_RESTART標(biāo)志有效時(shí),實(shí)現(xiàn)才重啟 動(dòng)系統(tǒng)調(diào)用。在10.14節(jié)將看到,sigaction函數(shù)使用這個(gè)標(biāo)志允許應(yīng)用 程序請(qǐng)求重啟動(dòng)被中斷的系統(tǒng)調(diào)用。
歷史上,使用signal函數(shù)建立信號(hào)處理程序時(shí),對(duì)于如何處理被中 斷的系統(tǒng)調(diào)用,各種實(shí)現(xiàn)的做法各不相同。System V的默認(rèn)工作方式是 從不重啟動(dòng)系統(tǒng)調(diào)用。而BSD則重啟動(dòng)被信號(hào)中斷的系統(tǒng)調(diào)用。FreeBSD 8.0、Linux 3.2.0和Mac OS X 10.6.8中,當(dāng)信號(hào)處理程序是用signal 函數(shù)時(shí),被中斷的系統(tǒng)調(diào)用會(huì)重啟動(dòng)。但 Solaris 10 的默認(rèn)方式是出 錯(cuò)返回,將 errno 設(shè)置為EINTR。使用用戶自己實(shí)現(xiàn)的signal函數(shù)(見 圖10-18)可以避免必須處理這些差異的麻煩。
4.2BSD引入自動(dòng)重啟動(dòng)功能的一個(gè)理由是:有時(shí)用戶并不知道所使 用的輸入、輸出設(shè)備是否是低速設(shè)備。如果我們編寫的程序可以用交互 方式運(yùn)行,則它可能讀、寫終端低速設(shè)備。如果在程序中捕捉信號(hào),而 且系統(tǒng)并不提供重啟動(dòng)功能,則對(duì)每次讀、寫系統(tǒng)調(diào)用就要進(jìn)行是否出 錯(cuò)返回的測(cè)試,如果是被中斷的,則再調(diào)用讀、寫系統(tǒng)調(diào)用。
圖10-3列出了幾種實(shí)現(xiàn)所提供的與信號(hào)有關(guān)的函數(shù)及它們的語義。

應(yīng)當(dāng)了解,其他廠商提供的UNIX系統(tǒng)可能不同于圖10-3中所示的 情況。例如,SunOS 4.1.2中的sigaction默認(rèn)方式是重啟動(dòng)被中斷的系統(tǒng) 調(diào)用,這與列在圖10-3中的各平臺(tái)不同。
在圖10-18中,提供了我們自己的signal函數(shù)版本,它自動(dòng)地嘗試重 啟動(dòng)被中斷的系統(tǒng)調(diào)用(除 SIGALRM信號(hào)外)。在圖10-19中則提供了 另一個(gè)函數(shù)signal_intr,它不進(jìn)行重啟動(dòng)。
在14.4節(jié)說明select和poll函數(shù)時(shí),還將更多涉及被中斷的系統(tǒng)調(diào) 用。

進(jìn)程捕捉到信號(hào)并對(duì)其進(jìn)行處理時(shí),進(jìn)程正在執(zhí)行的正常指令序列 就被信號(hào)處理程序臨時(shí)中斷,它首先執(zhí)行該信號(hào)處理程序中的指令。如 果從信號(hào)處理程序返回(例如沒有調(diào)用 exit 或longjmp),則繼續(xù)執(zhí)行在 捕捉到信號(hào)時(shí)進(jìn)程正在執(zhí)行的正常指令序列(這類似于發(fā)生硬件中斷時(shí) 所做的)。但在信號(hào)處理程序中,不能判斷捕捉到信號(hào)時(shí)進(jìn)程執(zhí)行到何 處。如果進(jìn)程正在執(zhí)行malloc,在其堆中分配另外的存儲(chǔ)空間,而此時(shí) 由于捕捉到信號(hào)而插入執(zhí)行該信號(hào)處理程序,其中又調(diào)用malloc,這時(shí) 會(huì)發(fā)生什么?又例如,若進(jìn)程正在執(zhí)行g(shù)etpwnam(見6.2節(jié))這種將其結(jié) 果存放在靜態(tài)存儲(chǔ)單元中的函數(shù),其間插入執(zhí)行信號(hào)處理程序,它又調(diào) 用這樣的函數(shù),這時(shí)又會(huì)發(fā)生什么呢?在malloc例子中,可能會(huì)對(duì)進(jìn)程 造成破壞,因?yàn)閙alloc通常為它所分配的存儲(chǔ)區(qū)維護(hù)一個(gè)鏈表,而插入 執(zhí)行信號(hào)處理程序時(shí),進(jìn)程可能正在更改此鏈表。在getpwnam的例子 中,返回給正常調(diào)用者的信息可能會(huì)被返回給信號(hào)處理程序的信息覆 蓋。

image.png

Single UNIX Specification說明了在信號(hào)處理程序中保證調(diào)用安全的 函數(shù)。這些函數(shù)是可重入的并被稱為是異步信號(hào)安全的(async-signal safe)。除了可重入以外,在信號(hào)處理操作期間,它會(huì)阻塞任何會(huì)引起 不一致的信號(hào)發(fā)送。圖10-4列出了這些異步信號(hào)安全的函數(shù)。沒有列入 圖10-4中的大多數(shù)函數(shù)是不可重入的,因?yàn)?a)已知它們使用靜態(tài)數(shù)據(jù) 結(jié)構(gòu);(b)它們調(diào)用 malloc 或free;(c)它們是標(biāo)準(zhǔn)I/O函數(shù)。標(biāo)準(zhǔn) I/O庫的很多實(shí)現(xiàn)都以不可重入方式使用全局?jǐn)?shù)據(jù)結(jié)構(gòu)。注意,雖然在 本書的某些實(shí)例中,信號(hào)處理程序也調(diào)用了printf函數(shù),但這并不保證產(chǎn) 生所期望的結(jié)果,信號(hào)處理程序可能中斷主程序中的printf函數(shù)調(diào)用。

應(yīng)當(dāng)了解,即使信號(hào)處理程序調(diào)用的是圖10-4中的函數(shù),但是由于
每個(gè)線程只有一個(gè)errno變量(回憶1.7節(jié)對(duì)errno和線程的討論),所以 信號(hào)處理程序可能會(huì)修改其原先值??紤]一個(gè)信號(hào)處理程序,它恰好在 main剛設(shè)置errno之后被調(diào)用。如果該信號(hào)處理程序調(diào)用read這類函數(shù), 則它可能更改errno的值,從而取代了剛由main設(shè)置的值。因此,作為一 個(gè)通用的規(guī)則,當(dāng)在信號(hào)處理程序中調(diào)用圖10-4中的函數(shù)時(shí),應(yīng)當(dāng)在調(diào) 用前保存errno,在調(diào)用后恢復(fù)errno。(應(yīng)當(dāng)了解,經(jīng)常被捕捉到的信 號(hào)是SIGCHLD,其信號(hào)處理程序通常要調(diào)用一種wait函數(shù),而各種wait 函數(shù)都能改變errno。)
注意,圖10-4沒有包括longjmp(7.10節(jié))和siglongjmp(10.15節(jié))。 這是因?yàn)橹骼桃苑强芍厝敕绞秸诟乱粋€(gè)數(shù)據(jù)結(jié)構(gòu)時(shí)可能產(chǎn)生信 號(hào)。如果不是從信號(hào)處理程序返回而是調(diào)用siglongjmp,那么該數(shù)據(jù)結(jié) 構(gòu)可能是部分更新的。如果應(yīng)用程序?qū)⒁龈氯謹(jǐn)?shù)據(jù)結(jié)構(gòu)這樣的事 情,而同時(shí)要捕捉某些信號(hào),而這些信號(hào)的處理程序又會(huì)引起執(zhí)行 siglongjmp,則在更新這種數(shù)據(jù)結(jié)構(gòu)時(shí)要阻塞此類信號(hào)。

10.7 SIGCLD語義

SIGCLD和SIGCHLD這兩個(gè)信號(hào)很容易被混淆。SIGCLD(沒有 H)是System V的一個(gè)信號(hào)名,其語義與名為SIGCHLD的BSD信號(hào)不 同。POSIX.1采用BSD的SIGCHLD信號(hào)。
BSD的SIGCHLD信號(hào)語義與其他信號(hào)的語義相類似。子進(jìn)程狀態(tài) 改變后產(chǎn)生此信號(hào),父進(jìn)程需要調(diào)用一個(gè)wait函數(shù)以檢測(cè)發(fā)生了什么。
System V處理SIGCLD信號(hào)的方式不同于其他信號(hào)。如果用signal或 sigset(早期設(shè)置信號(hào)配置的,與SRV3兼容的函數(shù))設(shè)置信號(hào)配置,則 基于SVR4的系統(tǒng)繼承了這一具有問題色彩的傳統(tǒng)(即兼容性限制)。 對(duì)于SIGCLD的早期處理方式是:
(1)如果進(jìn)程明確地將該信號(hào)的配置設(shè)置為SIG_IGN,則調(diào)用進(jìn) 程的子進(jìn)程將不產(chǎn)生僵死進(jìn)程。注意,這與其默認(rèn)動(dòng)作 (SIG_DFL)“忽略”(見圖10-1)不同。子進(jìn)程在終止時(shí),將其狀態(tài) 丟棄。如果調(diào)用進(jìn)程隨后調(diào)用一個(gè)wait函數(shù),那么它將阻塞直到所有子 進(jìn)程都終止,然后該wait會(huì)返回?1,并將其errno設(shè)置為ECHILD。(此 信號(hào)的默認(rèn)配置是忽略,但這不會(huì)使上述語義起作用。必須將其配置明 確指定為SIG_IGN才可以。)

我們需要先定義一些在討論信號(hào)時(shí)會(huì)用到的術(shù)語。首先,當(dāng)造成信 號(hào)的事件發(fā)生時(shí),為進(jìn)程產(chǎn)生一個(gè)信號(hào)(或向一個(gè)進(jìn)程發(fā)送一個(gè)信 號(hào))。事件可以是硬件異常(如除以 0)、軟件條件(如alarm 定時(shí)器超 時(shí))、終端產(chǎn)生的信號(hào)或調(diào)用kill 函數(shù)。當(dāng)一個(gè)信號(hào)產(chǎn)生時(shí),內(nèi)核通常 在進(jìn)程表中以某種形式設(shè)置一個(gè)標(biāo)志。
當(dāng)對(duì)信號(hào)采取了這種動(dòng)作時(shí),我們說向進(jìn)程遞送了一個(gè)信號(hào)。在信 號(hào)產(chǎn)生(generation)和遞送(delivery)之間的時(shí)間間隔內(nèi),稱信號(hào)是未 決的(pending)。
進(jìn)程可以選用“阻塞信號(hào)遞送”。如果為進(jìn)程產(chǎn)生了一個(gè)阻塞的信 號(hào),而且對(duì)該信號(hào)的動(dòng)作是系統(tǒng)默認(rèn)動(dòng)作或捕捉該信號(hào),則為該進(jìn)程將 此信號(hào)保持為未決狀態(tài),直到該進(jìn)程對(duì)此信號(hào)解除了阻塞,或者將對(duì)此 信號(hào)的動(dòng)作更改為忽略。內(nèi)核在遞送一個(gè)原來被阻塞的信號(hào)給進(jìn)程時(shí) (而不是在產(chǎn)生該信號(hào)時(shí)),才決定對(duì)它的處理方式。于是進(jìn)程在信號(hào) 遞送給它之前仍可改變對(duì)該信號(hào)的動(dòng)作。進(jìn)程調(diào)用sigpending函數(shù)(見 10.13節(jié))來判定哪些信號(hào)是設(shè)置為阻塞并處于未決狀態(tài)的。
如果在進(jìn)程解除對(duì)某個(gè)信號(hào)的阻塞之前,這種信號(hào)發(fā)生了多次,那 么將如何呢?POSIX.1允許系統(tǒng)遞送該信號(hào)一次或多次。如果遞送該信 號(hào)多次,則稱這些信號(hào)進(jìn)行了排隊(duì)。但是除非支持POSIX.1實(shí)時(shí)擴(kuò)展, 否則大多數(shù)UNIX并不對(duì)信號(hào)排隊(duì),而是只遞送這種信號(hào)一次。

10.9 函數(shù)kill和raise

kill函數(shù)將信號(hào)發(fā)送給進(jìn)程或進(jìn)程組。raise函數(shù)則允許進(jìn)程向自身發(fā) 送信號(hào)。
raise最初是由ISO C定義的。后來,為了與ISO C標(biāo)準(zhǔn)保持一致, POSIX.1也包括了該函數(shù)。但是POSIX.1擴(kuò)展了raise的規(guī)范,使其可處 理線程(12.8中討論線程如何與信號(hào)交互)。
因?yàn)镮SO C并不涉及多進(jìn)程,所以它不能定義以進(jìn)程ID作為其參數(shù) (如kill函數(shù))的函數(shù)。

#include <signal.h>
int kill(pid_t pid, int signo); int raise(int signo);

調(diào)用raise(signo);
等價(jià)于調(diào)用兩個(gè)函數(shù)返回值:若成功,返回0;若出錯(cuò),返回?1

kill(getpid(), signo);
kill的pid參數(shù)有以下4種不同的情況。

pid > 0 將該信號(hào)發(fā)送給進(jìn)程ID為pid的進(jìn)程。
pid == 0 將該信號(hào)發(fā)送給與發(fā)送進(jìn)程屬于同一進(jìn)程組的所有進(jìn)程(這些進(jìn)程的進(jìn)程組 ID等于發(fā)送進(jìn)程的進(jìn)程組 ID),而且發(fā)送進(jìn)程具 有權(quán)限向這些進(jìn)程發(fā)送信號(hào)。這里用的術(shù)語“所有進(jìn)程”不包括實(shí)現(xiàn)定 義的系統(tǒng)進(jìn)程集。對(duì)于大多數(shù)UNIX系統(tǒng),系統(tǒng)進(jìn)程集包括內(nèi)核進(jìn)程和 init(pid為1)。
pid < 0 將該信號(hào)發(fā)送給其進(jìn)程組ID等于pid絕對(duì)值,而且發(fā)送進(jìn)程 具有權(quán)限向其發(fā)送信號(hào)的所有進(jìn)程。如前所述,所有進(jìn)程并不包括系統(tǒng) 進(jìn)程集中的進(jìn)程。
pid == ?1 將該信號(hào)發(fā)送給發(fā)送進(jìn)程有權(quán)限向它們發(fā)送信號(hào)的所有 進(jìn)程。如前所述,所有進(jìn)程不包括系統(tǒng)進(jìn)程集中的進(jìn)程。

如前所述,進(jìn)程將信號(hào)發(fā)送給其他進(jìn)程需要權(quán)限。超級(jí)用戶可將信 號(hào)發(fā)送給任一進(jìn)程。對(duì)于非超級(jí)用戶,其基本規(guī)則是發(fā)送者的實(shí)際用戶 ID 或有效用戶 ID 必須等于接收者的實(shí)際用戶 ID或有效用戶ID。如果 實(shí)現(xiàn)支持_POSIX_SAVED_IDS(如POSIX.1現(xiàn)在要求的那樣),則檢查 接收者的保存設(shè)置用戶ID(而不是有效用戶ID)。在對(duì)權(quán)限進(jìn)行測(cè)試 時(shí)也有一個(gè)特例:如果被發(fā)送的信號(hào)是SIGCONT,則進(jìn)程可將它發(fā)送 給屬于同一會(huì)話的任一其他進(jìn)程。

10.11 信號(hào)集

我們需要有一個(gè)能表示多個(gè)信號(hào)——信號(hào)集(signal set)的數(shù)據(jù)類 型。我們將在sigprocmask (下一節(jié)中說明)類函數(shù)中使用這種數(shù)據(jù)類 型,以便告訴內(nèi)核不允許發(fā)生該信號(hào)集中的信號(hào)。如前所述,不同的信 號(hào)的編號(hào)可能超過一個(gè)整型量所包含的位數(shù),所以一般而言,不能用整 型量中的一位代表一種信號(hào),也就是不能用一個(gè)整型量表示信號(hào)集。 POSIX.1定義數(shù)據(jù)類型sigset_t以包含一個(gè)信號(hào)集,并且定義了下列5個(gè)處 理信號(hào)集的函數(shù)。

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);
4個(gè)函數(shù)返回值:若成功,返回0;若出錯(cuò),返回?1
int sigismember(const sigset_t *set, int signo);
返回值:若真,返回1;若假,返回0 函數(shù)sigemptyset初始化由set指向的信號(hào)集,清除其中所有信號(hào)。函
數(shù)sigfillset初始化由set指向的信號(hào)集,使其包括所有信號(hào)。所有應(yīng)用程序 在使用信號(hào)集前,要對(duì)該信號(hào)集調(diào)用sigemptyset或sigfillset一次。這是因 為C編譯程序?qū)⒉毁x初值的外部變量和靜態(tài)變量都初始化為0,而這是否 與給定系統(tǒng)上信號(hào)集的實(shí)現(xiàn)相對(duì)應(yīng)卻并不清楚。

一旦已經(jīng)初始化了一個(gè)信號(hào)集,以后就可在該信號(hào)集中增、刪特定 的信號(hào)。函數(shù) sigaddset將一個(gè)信號(hào)添加到已有的信號(hào)集中,sigdelset 則從信號(hào)集中刪除一個(gè)信號(hào)。對(duì)所有以信號(hào)集作為參數(shù)的函數(shù),總是以信 號(hào)集地址作為向其傳送的參數(shù)。

如果實(shí)現(xiàn)的信號(hào)數(shù)目少于一個(gè)整型量所包含的位數(shù),則可用一位代 表一個(gè)信號(hào)的方法實(shí)現(xiàn)信號(hào)集。例如,本書的后續(xù)部分都假定一種實(shí)現(xiàn) 有31種信號(hào)和32位整型。sigemptyset函數(shù)將整型設(shè)置為0, sigfillset函數(shù) 則將整型中的各位都設(shè)置為1。這兩個(gè)函數(shù)可以在<signal.h>頭文件中實(shí) 現(xiàn)為宏:

#define sigemptyset(ptr) (*(ptr) = 0)
#define sigfillset(ptr) (*(ptr) = ~(sigset_t)0, 0)

注意,除了設(shè)置信號(hào)集中各位為1外,sigfillset必須返回0,所以使用
C語言的逗號(hào)算符,它將逗號(hào)算符后的值作為表達(dá)式的值返回。 使用這種實(shí)現(xiàn),sigaddset 開啟一位(將該位設(shè)置為 1),sigdelset 則
關(guān)閉一位(將該位設(shè)置為0);sigismember測(cè)試一個(gè)指定的位。因?yàn)闆] 有信號(hào)編號(hào)為0,所以從信號(hào)編號(hào)中減1以得到要處理位的位編號(hào)數(shù)。圖 10-12給出了這些函數(shù)的實(shí)現(xiàn)。

10.12 函數(shù)sigprocmask

10.8節(jié)曾提及一個(gè)進(jìn)程的信號(hào)屏蔽字規(guī)定了當(dāng)前阻塞而不能遞送給 該進(jìn)程的信號(hào)集。調(diào)用函數(shù)sigprocmask可以檢測(cè)或更改,或同時(shí)進(jìn)行檢 測(cè)和更改進(jìn)程的信號(hào)屏蔽字。

#include <signal.h>
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);

返回值:若成功,返回0;若出錯(cuò),返回?1 首先,若oset是非空指針,那么進(jìn)程的當(dāng)前信號(hào)屏蔽字通過oset返回。
其次,若set是一個(gè)非空指針,則參數(shù)how指示如何修改當(dāng)前信號(hào)屏
蔽字。圖10-13說明了how可選的值。SIG_BLOCK是或操作,而 SIG_SETMASK則是賦值操作。注意,不能阻塞SIGKILL和SIGSTOP信 號(hào)。


image.png

10.13 函數(shù)sigpending

sigpending函數(shù)返回一信號(hào)集,對(duì)于調(diào)用進(jìn)程而言,其中的各信號(hào)是 阻塞不能遞送的,因而也一定是當(dāng)前未決的。該信號(hào)集通過set參數(shù)返 回。

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

返回值:若成功,返回0;若出錯(cuò),返回?1

#include <iostream>
#include <stdio.h>
#include <sys/signal.h>
#include <signal.h>
#include <unistd.h>

static void sig_quit(int signo) {
    std::cout << "sigprocess, sigpending" << std::endl;
}

int main() {
    sigset_t newmask, oldmask, pendmask;

    if(signal(SIGTSTP, sig_quit) == SIG_ERR) {
        printf("signal err");
    }

    /* Block SIGQUIT. */
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGTSTP);

    if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) {
        printf("SIG Block error\n");
    }
    sleep(5);
    
    if(sigpending(&pendmask) < 0) {
        printf("sigpending error");
    }

    if(sigismember(&pendmask, SIGTSTP)) {
        printf("sigpending : SIGTSTP");
    }
}

10.14 函數(shù)sigaction

sigaction函數(shù)的功能是檢查或修改(或檢查并修改)與指定信號(hào)相 關(guān)聯(lián)的處理動(dòng)作。此函數(shù)取代了UNIX早期版本使用的signal函數(shù)。在本 節(jié)末尾用sigaction函數(shù)實(shí)現(xiàn)了signal。

#include <signal.h>
int sigaction(int signo, const struct sigaction *restrict act,
          struct sigaction *restrict oact);

返回值:若成功,返回0;若出錯(cuò),返回?1 其中,參數(shù)signo是要檢測(cè)或修改其具體動(dòng)作的信號(hào)編號(hào)。若act指針
非空,則要修改其動(dòng)作。如果oact指針非空,則系統(tǒng)經(jīng)由oact指針返回 該信號(hào)的上一個(gè)動(dòng)作。此函數(shù)使用下列結(jié)構(gòu):

struct sigaction {
void   (*sa_handler)(int); /* addr of signal handler,or SIG_IGN, or SIG_DFL */
sigset_t sa_mask  */block */
int   sa_flags;      /* signal options, Figure 10.16 */
/* alternate handler */ 
void  (*sa_sigaction)(int, siginfo_t *, void *);}; 

當(dāng)更改信號(hào)動(dòng)作時(shí),如果 sa_handler 字段包含一個(gè)信號(hào)捕捉函數(shù)的 地址(不是常量SIG_IGN或SIG_DFL),則sa_mask字段說明了一個(gè)信 號(hào)集,在調(diào)用該信號(hào)捕捉函數(shù)之前,這一信號(hào)集要加到進(jìn)程的信號(hào)屏蔽 字中。僅當(dāng)從信號(hào)捕捉函數(shù)返回時(shí)再將進(jìn)程的信號(hào)屏蔽字恢復(fù)為原先 值。這樣,在調(diào)用信號(hào)處理程序時(shí)就能阻塞某些信號(hào)。在信號(hào)處理程序 被調(diào)用時(shí),操作系統(tǒng)建立的新信號(hào)屏蔽字包括正被遞送的信號(hào)。因此保 證了在處理一個(gè)給定的信號(hào)時(shí),如果這種信號(hào)再次發(fā)生,那么它會(huì)被阻 塞到對(duì)前一個(gè)信號(hào)的處理結(jié)束為止?;貞?0.8節(jié),若同一種信號(hào)多次發(fā) 生,通常并不將它們加入隊(duì)列,所以如果在某種信號(hào)被阻塞時(shí),它發(fā)生 了5次,那么對(duì)這種信號(hào)解除阻塞后,其信號(hào)處理函數(shù)通常只會(huì)被調(diào)用 一次(上一個(gè)例子已經(jīng)說明了這種特性)。
一旦對(duì)給定的信號(hào)設(shè)置了一個(gè)動(dòng)作,那么在調(diào)用sigaction顯式地改 變它之前,該設(shè)置就一直有效。這種處理方式與早期的不可靠信號(hào)機(jī)制 不同,符合POSIX.1在這方面的要求。
act結(jié)構(gòu)的sa_flags字段指定對(duì)信號(hào)進(jìn)行處理的各個(gè)選項(xiàng)。圖10-16詳 細(xì)列出了這些選項(xiàng)的意義。若該標(biāo)志已定義在基本 POSIX.1 標(biāo)準(zhǔn)中,那 么 SUS 列包含“?”;若該標(biāo)志定義在基本POSIX.1標(biāo)準(zhǔn)的XSI擴(kuò)展中, 那么該列包含“XSI”。

image.png

sa_sigaction字段是一個(gè)替代的信號(hào)處理程序,在sigaction結(jié)構(gòu)中使用 了SA_SIGINFO標(biāo)志時(shí),使用該信號(hào)處理程序。對(duì)于sa_sigaction字段和 sa_handler字段兩者,實(shí)現(xiàn)可能使用同一存儲(chǔ)區(qū),所以應(yīng)用只能一次使用 這兩個(gè)字段中的一個(gè)。
通常,按下列方式調(diào)用信號(hào)處理程序:
void handler(int signo);
但是,如果設(shè)置了SA_SIGINFO標(biāo)志,那么按下列方式調(diào)用信號(hào)處
理程序:
void handler(int signo, siginfo_t *info, void context);
siginfo結(jié)構(gòu)包含了信號(hào)產(chǎn)生原因的有關(guān)信息。該結(jié)構(gòu)的大致樣式如 下所示。符合POSIX.1的所有實(shí)現(xiàn)必須至少包括si_signo和si_code成員。 另外,符合XSI的實(shí)現(xiàn)至少應(yīng)包含下列字段:
struct siginfo {
int si_signo; /
signal number /
int si_errno; /
if nonzero, errno value from
<errno.h> /
int si_code; /
additional info (depends on
signal) /
pid_t si_pid; /
sending process ID / uid_t si_uid; / sending process real user
ID */
void si_addr; / address that caused the
fault /
int si_status; /
exit value or signal number
/
union sigval si_value; /
application-specific value / / possibly other fields also */
};
sigval聯(lián)合包含下列字段:
int sival_int;
void *sival_ptr; 應(yīng)用程序在遞送信號(hào)時(shí),在si_value.sival_int中傳遞一個(gè)整型數(shù)或者
在si_value.sival_ptr中傳遞一個(gè)指針值。 圖10-17示出了對(duì)于各種信號(hào)的si_code值,這些信號(hào)是由Single

UNIX Specification定義的。注意,實(shí)現(xiàn)可定義附加的代碼值。 若信號(hào)是SIGCHLD,則將設(shè)置si_pid、si_status和si_uid字段。若信
號(hào)是SIGBUS、SIGILL、SIGFPE或SIGSEGV,則si_addr包含造成故障的 根源地址,該地址可能并不準(zhǔn)確。si_errno字段包含錯(cuò)誤編號(hào),它對(duì)應(yīng)于 造成信號(hào)產(chǎn)生的條件,并由實(shí)現(xiàn)定義。
信號(hào)處理程序的context參數(shù)是無類型指針,它可被強(qiáng)制類型轉(zhuǎn)換為 ucontext_t結(jié)構(gòu)類型,該結(jié)構(gòu)標(biāo)識(shí)信號(hào)傳遞時(shí)進(jìn)程的上下文。該結(jié)構(gòu)至少 包含下列字段:

ucontext_t *uc_link;    /* pointer to context resumed when */
sigset_t  uc_sigmask;  /* signals blocked when this context */
stack_t   uc_stack;   /* stack used by this context */
/* this context returns */
/* is active */
mcontext_t uc_mcontext; /* machine-specific representation
of */
/* saved context */ uc_stack字段描述了當(dāng)前上下文使用的棧,至少包括下列成員: void *ss_sp;       /* stack base or pointer */ size_t ss_size;      /* stack size */ int  ss_flags;      /* flags */ 

當(dāng)實(shí)現(xiàn)支持實(shí)時(shí)信號(hào)擴(kuò)展時(shí),用SA_SIGINFO標(biāo)志建立的信號(hào)處理程序?qū)⒃斐尚盘?hào)可靠地排隊(duì)。一些保留信號(hào)可由實(shí)時(shí)應(yīng)用使用。如果信號(hào) 由sigqueue函數(shù)產(chǎn)生,那么siginfo結(jié)構(gòu)能包含應(yīng)用特有的數(shù)據(jù)(參見 10.20節(jié))。

10.16sigsuspend

10.16 函數(shù)sigsuspend
上面已經(jīng)說明,更改進(jìn)程的信號(hào)屏蔽字可以阻塞所選擇的信號(hào),或 解除對(duì)它們的阻塞。使用這種技術(shù)可以保護(hù)不希望由信號(hào)中斷的代碼臨 界區(qū)。如果希望對(duì)一個(gè)信號(hào)解除阻塞,然后pause以等待以前被阻塞的信 號(hào)發(fā)生,則又將如何呢?假定信號(hào)是SIGINT,實(shí)現(xiàn)這一點(diǎn)的一種不正 確的方法是:
sigset_t newmask, oldmask; sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);
/* block SIGINT and save current signal mask /
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
err_sys("SIG_BLOCK error");
/
critical region of code /
/
restore signal mask, which unblocks SIGINT / if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
err_sys("SIG_SETMASK error");
/
window is open /
pause(); /
wait for signal to occur /
/
continue processing */ 如果在信號(hào)阻塞時(shí),產(chǎn)生了信號(hào),那么該信號(hào)的傳遞就被推遲直到
對(duì)它解除了阻塞。對(duì)應(yīng)用程序而言,該信號(hào)好像發(fā)生在解除對(duì)SIGINT 的阻塞和pause之間(取決于內(nèi)核如何實(shí)現(xiàn)信號(hào))。如果發(fā)生了這種情 況,或者如果在解除阻塞時(shí)刻和 pause 之間確實(shí)發(fā)生了信號(hào),那么就會(huì)

產(chǎn)生問題。因?yàn)榭赡懿粫?huì)再見到該信號(hào),所以從這種意義上講,在此時(shí) 間窗口中發(fā)生的信號(hào)丟失了,這樣就使得pause永遠(yuǎn)阻塞。這是早期的不 可靠信號(hào)機(jī)制的另一個(gè)問題。
為了糾正此問題,需要在一個(gè)原子操作中先恢復(fù)信號(hào)屏蔽字,然后 使進(jìn)程休眠。這種功能是由sigsuspend函數(shù)所提供的。

include <signal.h>

int sigsuspend(const sigset_t *sigmask);
返回值:?1,并將errno設(shè)置為EINTR 進(jìn)程的信號(hào)屏蔽字設(shè)置為由sigmask指向的值。在捕捉到一個(gè)信號(hào)或 發(fā)生了一個(gè)會(huì)終止該進(jìn)程的信號(hào)之前,該進(jìn)程被掛起。如果捕捉到一個(gè)
信號(hào)而且從該信號(hào)處理程序返回,則sigsuspend返回,并且該進(jìn)程的信號(hào) 屏蔽字設(shè)置為調(diào)用sigsuspend之前的值。
注意,此函數(shù)沒有成功返回值。如果它返回到調(diào)用者,則總是返回 ?1,并將 errno 設(shè)置為EINTR(表示一個(gè)被中斷的系統(tǒng)調(diào)用)。
實(shí)例
圖10-22顯示了保護(hù)代碼臨界區(qū),使其不被特定信號(hào)中斷的正確方 法。

圖10-22 保護(hù)臨界區(qū)不被信號(hào)中斷 注意,當(dāng)sigsuspend返回時(shí),它將信號(hào)屏蔽字設(shè)置為調(diào)用它之前的

值。在本例中,SIGINT信號(hào)將被阻塞。因此將信號(hào)屏蔽恢復(fù)為之前保 存的值(oldmask)。
運(yùn)行圖10-22中的程序得到下面的輸出:

$ ./a.out
program start:
in critical region: SIGINT
^C                鍵入中斷字符
in sig_int: SIGINT SIGUSR1
after return from sigsuspend: SIGINT
program exit:

在調(diào)用sigsuspend時(shí),將SIGUSRI信號(hào)加到了進(jìn)程信號(hào)屏蔽字中,所以當(dāng)運(yùn)行該信號(hào)處理程序時(shí),我們得知信號(hào)屏蔽字已經(jīng)改變了。從中可 見,在 sigsuspend 返回時(shí),它將信號(hào)屏蔽字恢復(fù)為調(diào)用它之前的值。
實(shí)例
sigsuspend的另一種應(yīng)用是等待一個(gè)信號(hào)處理程序設(shè)置一個(gè)全局變 量。圖10-23中的程序用于捕捉中斷信號(hào)和退出信號(hào),但是希望僅當(dāng)捕捉 到退出信號(hào)時(shí),才喚醒主例程。

10.17 abort

10.17 函數(shù)abort
前面已提及abort函數(shù)的功能是使程序異常終止。

#include <stdlib.h> 
void abort(void);

此函數(shù)不返回值 此函數(shù)將SIGABRT信號(hào)發(fā)送給調(diào)用進(jìn)程(進(jìn)程不應(yīng)忽略此信號(hào))。
ISO C規(guī)定,調(diào)用abort將向主機(jī)環(huán)境遞送一個(gè)未成功終止的通知,其方 法是調(diào)用raise(SIGABRT)函數(shù)。

10.18 system 函數(shù)

10.19 sleep nanosleep和clock_nanosleep

在本書的很多例子中都已使用了sheep函數(shù),在圖10-7程序和圖10-8 程序中有兩個(gè)sleep的實(shí)現(xiàn),但它們都是有缺陷的。

#include <unistd.h>
unsigned int sleep(unsigned int seconds);

返回值:0或未休眠完的秒數(shù) 此函數(shù)使調(diào)用進(jìn)程被掛起直到滿足下面兩個(gè)條件之一。
(1)已經(jīng)過了seconds所指定的墻上時(shí)鐘時(shí)間。 (2)調(diào)用進(jìn)程捕捉到一個(gè)信號(hào)并從信號(hào)處理程序返回。

#include <time.h>
int nanosleep(const struct timespec *reqtp, struct timespec *remtp);

返回值:若休眠到要求的時(shí)間,返回0;若出錯(cuò),返回?1 這個(gè)函數(shù)掛起調(diào)用進(jìn)程,直到要求的時(shí)間已經(jīng)超時(shí)或者某個(gè)信號(hào)中 斷了該函數(shù)。reqtp參數(shù)用秒和納秒指定了需要休眠的時(shí)間長度。如果某
個(gè)信號(hào)中斷了休眠間隔,進(jìn)程并沒有終止,remtp參數(shù)指向的 timespec 結(jié)構(gòu)就會(huì)被設(shè)置為未休眠完的時(shí)間長度。如果對(duì)未休眠完的時(shí)間并不感 興趣,可以把該參數(shù)置為NULL。
如果系統(tǒng)并不支持納秒這一精度,要求的時(shí)間就會(huì)取整。因?yàn)?nanosleep函數(shù)并不涉及產(chǎn)生任何信號(hào),所以不需要擔(dān)心與其他函數(shù)的交 互。

隨著多個(gè)系統(tǒng)時(shí)鐘的引入(回憶 6.10 節(jié)),需要使用相對(duì)于特定時(shí) 鐘的延遲時(shí)間來掛起調(diào)用線程。clock_nanosleep函數(shù)提供了這種功能。

#include <time.h>
int clock_nanosleep(clockid_t clock_id, int flags,
const struct timespec *reqtp, struct timespec *remtp);

返回值:若休眠要求的時(shí)間,返回0;若出錯(cuò),返回錯(cuò)誤碼 clock_id參數(shù)指定了計(jì)算延遲時(shí)間基于的時(shí)鐘。時(shí)鐘標(biāo)識(shí)符列于圖6-
8中。flags參數(shù)用于控制延遲是相對(duì)的還是絕對(duì)的。flags為0時(shí)表示休眠 時(shí)間是相對(duì)的(例如,希望休眠的時(shí)間長度),如果flags值設(shè)置為 TIMER_ABSTIME,表示休眠時(shí)間是絕對(duì)的(例如,希望休眠到時(shí)鐘到 達(dá)某個(gè)特定的時(shí)間)。
其他的參數(shù)reqtp和remtp,與nanosleep函數(shù)中的相同。但是,使用
絕對(duì)時(shí)間時(shí),remtp參數(shù)未使用,因?yàn)闆]有必要。在時(shí)鐘到達(dá)指定的絕 對(duì)時(shí)間值以前,可以為其他的clock_nanosleep調(diào)用復(fù)用reqtp參數(shù)相同的 值。
注意,除了出錯(cuò)返回,調(diào)用
clock_nanosleep(CLOCK_REALTIME, 0, reqtp, remtp);
和調(diào)用
nanosleep(reqtp, remtp);
的效果是相同的。使用相對(duì)休眠的問題是有些應(yīng)用對(duì)休眠長度有精 度要求,相對(duì)休眠時(shí)間會(huì)導(dǎo)致實(shí)際休眠時(shí)間比要求的長。例如,某個(gè)應(yīng) 用程序希望按固定的時(shí)間間隔執(zhí)行任務(wù),就必須獲取當(dāng)前時(shí)間,計(jì)算下 次執(zhí)行任務(wù)的時(shí)間,然后調(diào)用nanosleep。在獲取當(dāng)前時(shí)間和調(diào)用 nanosleep之間,處理器調(diào)度和搶占可能會(huì)導(dǎo)致相對(duì)休眠時(shí)間超過實(shí)際需 要的時(shí)間間隔。即便分時(shí)進(jìn)程調(diào)度程序?qū)π菝邥r(shí)間結(jié)束后是否會(huì)馬上執(zhí) 行用戶任務(wù)并沒有給出保證,使用絕對(duì)時(shí)間還是改善了精度。

10.20 函數(shù)sigqueue

在10.8節(jié)中,我們介紹了大部分UNIX系統(tǒng)不對(duì)信號(hào)排隊(duì)。在 POSIX.1的實(shí)時(shí)擴(kuò)展中,有些系統(tǒng)開始增加對(duì)信號(hào)排隊(duì)的支持。在SUSv4 中,排隊(duì)信號(hào)功能已從實(shí)時(shí)擴(kuò)展部分移至基礎(chǔ)說明部分。
通常一個(gè)信號(hào)帶有一個(gè)位信息:信號(hào)本身。除了對(duì)信號(hào)排隊(duì)以外, 這些擴(kuò)展允許應(yīng)用程序在遞交信號(hào)時(shí)傳遞更多的信息(回憶10.14節(jié))。 這些信息嵌入在siginfo結(jié)構(gòu)中。除了系統(tǒng)提供的信息,應(yīng)用程序還可以 向信號(hào)處理程序傳遞整數(shù)或者指向包含更多信息的緩沖區(qū)指針。
使用排隊(duì)信號(hào)必須做以下幾個(gè)操作。
(1)使用sigaction函數(shù)安裝信號(hào)處理程序時(shí)指定SA_SIGINFO標(biāo) 志。如果沒有給出這個(gè)標(biāo)志,信號(hào)會(huì)延遲,但信號(hào)是否進(jìn)入隊(duì)列要取決 于具體實(shí)現(xiàn)。
(2)在sigaction結(jié)構(gòu)的sa_sigaction成員中(而不是通常的sa_handler 字段)提供信號(hào)處理程序。實(shí)現(xiàn)可能允許用戶使用sa_handler字段,但不 能獲取sigqueue函數(shù)發(fā)送出來的額外信息。
(3)使用sigqueue函數(shù)發(fā)送信號(hào)。

include <signal.h>

int sigqueue(pid_t pid, int signo, const union sigval value);
返回值:若成功,返回0;若出錯(cuò),返回?1 sigqueue函數(shù)只能把信號(hào)發(fā)送給單個(gè)進(jìn)程,可以使用value參數(shù)向信
號(hào)處理程序傳遞整數(shù)和指針值,除此之外,sigqueue函數(shù)與kill函數(shù)類 似。

信號(hào)不能被無限排隊(duì)。回憶圖2-9和圖2-11中的SIGQUEUE_MAX限 制。到達(dá)相應(yīng)的限制以后,sigqueue就會(huì)失敗,將errno設(shè)為EAGAIN。
隨著實(shí)時(shí)信號(hào)的增強(qiáng),引入了用于應(yīng)用程序的獨(dú)立信號(hào)集。這些信 號(hào)的編號(hào)在SIGRTMIN~SIGRTMAX之間,包括這兩個(gè)限制值。注意, 這些信號(hào)的默認(rèn)行為是終止進(jìn)程。
圖10-30總結(jié)了排隊(duì)信號(hào)在本書不同的實(shí)現(xiàn)中的行為上的差異。
Mac OS X 10.6.8并不支持sigqueue或者實(shí)時(shí)信號(hào)。在Solaris 10 中,sigqueue在實(shí)時(shí)庫librt中。
圖10-30 不同平臺(tái)上排隊(duì)信號(hào)的行為


image.png

10.21 作業(yè)控制信號(hào)

在圖10-1所示的信號(hào)中,POSIX.1認(rèn)為有以下6個(gè)與作業(yè)控制有關(guān)。 SIGCHLD 子進(jìn)程已停止或終止。
SIGCONT 如果進(jìn)程已停止,則使其繼續(xù)運(yùn)行。
SIGSTOP 停止信號(hào)(不能被捕捉或忽略)。
SIGTSTP 交互式停止信號(hào)。
SIGTTIN 后臺(tái)進(jìn)程組成員讀控制終端。
SIGTTOU 后臺(tái)進(jìn)程組成員寫控制終端。 除SIGCHLD以外,大多數(shù)應(yīng)用程序并不處理這些信號(hào),交互式shell
則通常會(huì)處理這些信號(hào)的所有工作。當(dāng)鍵入掛起字符(通常是Ctrl+Z) 時(shí),SIGTSTP被送至前臺(tái)進(jìn)程組的所有進(jìn)程。當(dāng)我們通知shell在前臺(tái)或 后臺(tái)恢復(fù)運(yùn)行一個(gè)作業(yè)時(shí),shell向該作業(yè)中的所有進(jìn)程發(fā)送SIGCONT信 號(hào)。與此類似,如果向一個(gè)進(jìn)程遞送了SIGTTIN或SIGTTOU信號(hào),則 根據(jù)系統(tǒng)默認(rèn)的方式,停止此進(jìn)程,作業(yè)控制shell了解到這一點(diǎn)后就通 知我們。
一個(gè)例外是管理終端的進(jìn)程,例如,vi(1)編輯器。當(dāng)用戶要掛起它 時(shí),它需要能了解到這一點(diǎn),這樣就能將終端狀態(tài)恢復(fù)到 vi 啟動(dòng)時(shí)的情 況。另外,當(dāng)在前臺(tái)恢復(fù)它時(shí),它需要將終端狀態(tài)設(shè)置回它所希望的狀 態(tài),并需要重新繪制終端屏幕??梢栽谙旅娴睦又杏^察到與 vi 類似的 程序是如何處理這種情況的。
在作業(yè)控制信號(hào)間有某些交互。當(dāng)對(duì)一個(gè)進(jìn)程產(chǎn)生 4 種停止信號(hào) (SIGTSTP、SIGSTOP、SIGTTIN或SIGTTOU)中的任意一種時(shí),對(duì)該 進(jìn)程的任一未決SIGCONT信號(hào)就被丟棄。與此類似,當(dāng)對(duì)一個(gè)進(jìn)程產(chǎn)生SIGCONT信號(hào)時(shí),對(duì)同一進(jìn)程的任一未決停止信號(hào)被丟棄。 注意,如果進(jìn)程是停止的,則SIGCONT的默認(rèn)動(dòng)作是繼續(xù)該進(jìn)程;否則忽略此信號(hào)。通常,對(duì)該信號(hào)無需做任何事情。當(dāng)對(duì)一個(gè)停止 的進(jìn)程產(chǎn)生一個(gè) SIGCONT 信號(hào)時(shí),該進(jìn)程就繼續(xù),即使該信號(hào)是被阻 塞或忽略的也是如此。

10.22 信號(hào)名和編號(hào)

本節(jié)介紹如何在信號(hào)編號(hào)和信號(hào)名之間進(jìn)行映射。某些系統(tǒng)提供數(shù)

extern char *sys_siglist[];

數(shù)組下標(biāo)是信號(hào)編號(hào),數(shù)組中的元素是指向信號(hào)名符串的指針。 FreeBSD 8.0、Linux 3.2.0和Mac OS X 10.6.8都提供這種信號(hào)名
數(shù)組。Solaris 10也提供信號(hào)名數(shù)組,但該數(shù)組名是_sys_siglist。
可以使用psignal函數(shù)可移植地打印與信號(hào)編號(hào)對(duì)應(yīng)的字符串。

#include <signal.h>
void psignal(int signo, const char *msg); 

字符串msg(通常是程序名)輸出到標(biāo)準(zhǔn)錯(cuò)誤文件,后面跟隨一個(gè)
冒號(hào)和一個(gè)空格,再后面對(duì)該信號(hào)的說明,最后是一個(gè)換行符。如果 msg為NULL,只有信號(hào)說明部分輸出到標(biāo)準(zhǔn)錯(cuò)誤文件,該函數(shù)類似于 perror(1.7節(jié))。
如果在sigaction信號(hào)處理程序中有siginfo結(jié)構(gòu),可以使用psiginfo函數(shù) 打印信號(hào)信息。

#include <signal.h>
void psiginfo(const siginfo_t *info, const char *msg);

它的工作方式與 psignal 函數(shù)類似。雖然這個(gè)函數(shù)訪問除信號(hào)編號(hào)以
外的更多信息,但不同的平臺(tái)輸出的這些額外信息可能有所不同。 如果只需要信號(hào)的字符描述部分,也不需要把它寫到標(biāo)準(zhǔn)錯(cuò)誤文件
中(如可以寫到日志文件中),可以使用strsignal函數(shù),它類似于 strerror(另見1.7節(jié))。

#include <string.h>
char *strsignal(int signo);

返回值:指向描述該信號(hào)的字符串的指針 給出一個(gè)信號(hào)編號(hào),strsignal 將返回描述該信號(hào)的字符串。應(yīng)用程
序可用該字符串打印關(guān)于接收到信號(hào)的出錯(cuò)消息。
本書討論的所有平臺(tái)都提供psignal和strsignal函數(shù),但相互之間 有些差別。在Solaris 10中,若信號(hào)編號(hào)無效,strsignal將返回一個(gè) 空指針,而FreeBSD 8.0、Linux 3.2.0和Mac OS X 10.6.8則返回一個(gè) 字符串,它指出信號(hào)編號(hào)是不可識(shí)別的。
只有Linux 3.2.0和Solaris 10支持psiginfo函數(shù)。
Solaris提供一對(duì)函數(shù),一個(gè)函數(shù)將信號(hào)編號(hào)映射為信號(hào)名,另一個(gè) 則反之。

#include <signal.h>
int sig2str(int signo, char *str);
int str2sig(const char *str, int *signop);

兩個(gè)函數(shù)的返回值:若成功,返回0;若出錯(cuò),返回?1 在編寫交互式程序,其中需接收和打印信號(hào)名和信號(hào)編號(hào)時(shí),這兩
個(gè)函數(shù)是有用的。 sig2str函數(shù)將給定信號(hào)編號(hào)翻譯成字符串,并將結(jié)果存放在str指向
的存儲(chǔ)區(qū)。調(diào)用者必須保證該存儲(chǔ)區(qū)足夠大,可以保存最長字符串,包 括終止 null 字節(jié)。Solaris 在<signal.h>中包含了常量SIG2STR_MAX,它 定義了最大字符串長度。該字符串包括不帶“SIG”前綴的信號(hào)名。例 如,SIGKILL被翻譯為字符串“KILL”,并存放在str指向的存儲(chǔ)緩沖區(qū) 中。
str2sig 函數(shù)將給出的信號(hào)名翻譯成信號(hào)編號(hào)。該信號(hào)編號(hào)存放在 signop指向的整型中。名字要么是不帶“SIG”前綴的信號(hào)名,要么是表 示十進(jìn)制信號(hào)編號(hào)的字符串(如“9”)。

注意,sig2str和str2sig與常用的函數(shù)做法不同,當(dāng)它們失敗時(shí),并不 設(shè)置errno。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容