《UNIX環境高級編程》第10章 信號

10.1 信號

信號是軟中斷。很多比較重要的應用程序都需處理信號。信號提供了一種處理異步事件的方法。例如,終端用戶鍵入中斷鍵,會通過信號機制停止一個程序,或及早地終止管道中的下一個程序。

10.2 信號概念

首先,每個信號都有一個名字。這些名字都是以3個字符SIG開頭。例如:

  • SIGABRT 是夭折信號,當進程調用abort函數時產生這種信號。
  • SIGALRM 是鬧鐘信號,由alarm函數設定的定時器超時後將產生此信號。
    各種系統實現的信號數量不同,linux支持31種信號。

不存在編號爲0的信號。很多條件可以產生信號。

  • 當用戶按某些終端鍵時,引發終端產生信號。在終端上按Delet鍵(很多系統上時是Ctrl+c)通常產生中斷信號(SIGINT)。這是停止一個已經失去控制的程序的方法。

  • 硬件異常產生信號:除數爲0、無效的內存引用等。這些條件通常由硬件檢測到,並通知內核。然後由內核爲該條件正在運行時的進程產生適當的信號。例如,對執行一個無效內存引用的進程產生SIGSEGV信號。

  • 進程調用kill函數可以將任意信號發送給另一個進程或進程組。限制是:接收信號進程和發送信號進程的所有者必須相同,或發送信號的進程必須是超級用戶。

  • 用戶可用kill命令將信號發送給其他進程。此命令只是kill函數的接口。常用此命令終止一個失控的後臺進程。

  • 當檢測到某種軟件條件已經發生,並將其通知有關進程時也產生信號。這裏不是指硬件產生條件,而是軟件條件。


信號是異步事件的經典實例。產生信號的事件對進程而言是隨機出現的。進程不能簡單地測試一個變量來判斷是否發生了一個信號(同步處理),而是必須告訴內核“此信號發生時,請執行下列操作”(異步處理)。

在某個信號出現時,可以告訴內核按下列3種方式之一進行處理,我們稱之爲信號的處理或與信號相關的動作。

  1. 忽略此信號。大多數信號都可以使用這種方式進行處理,但有兩種信號卻絕不能被忽略。它們是SIGKILL和SIGSTOP。這兩種信號不能被忽略的原因是:它們向內核和超級用戶提供了使進程終止或停止的可靠方法。另外,如果忽略某些有硬件異常產生的信號(如除以0),則信號的運行行爲是未定義的。

  2. 捕捉信號。爲了做到這一點,要通知內核在某種信號發生時,調用一個用戶函數。在用戶函數中,可執行用戶希望對這種事件進行的處理。注意,不能捕捉SIGKILL和SIGSTOP信號。

  3. 執行系統默認動作。對大多數信號的默認動作是終止該進程。

下面列出了所有信號的名字,以及其默認動作。“終止+core”表示在進程當前目錄的core文件中複製了該進程的內存映像,該文件名爲core大多數UNIX系統調試程序都使用core文件檢查進程終止時的狀態

名字 說明 默認動作
SIGABRT 異常終止(abort) 終止+core
SIGALRM 定時器超時(abort) 終止
SIGBUS 硬件故障 終止+core
SIGCHLD 子進程狀態改變 忽略
SIGCONT 使暫停進程繼續 繼續/忽略
SIGEMT 硬件故障 終止+core
SIGFPE 算術異常 終止+core
SIGHUP 連接斷開 終止
SIGILL 非法硬件質量 終止+core
SIGITN 終端中斷符 終止
SIGIO 異步IO 終止/忽略
SIGIOT 硬件故障 終止+core
SIGKILL 終止 終止
SIGPIPE 寫至無讀進程的管道 終止
SIGPOLL 可輪詢事件(poll) 終止
SIGPROF 梗概時間超時(setitimer) 終止
SIGPWR 電源失效/重啓動 終止/忽略
SIGQUIT 終端退出符 終止+core
SIGSEGV 無效內存引用 終止+core
SIGSTKFLT 協處理器棧故障 終止
SIGSTOP 停止 停止進程
SIGSYS 無效系統調用 終止+core
SIGTERM 終止 終止
SIGTRAP 硬件故障 終止+core
SIGTSTP 終端停止符 停止進程
SIGTTIN 後臺讀控制tty 停止進程
SIGTTOU 後臺寫向控制tty 停止進程
SIGGURG 緊急情況(套接字) 忽略
SIGGUSR1 用戶定義信號,可用於應用程序 終止
SIGGUSR2 用戶定義信號,可用於應用程序 終止
SIGVTALRM 虛擬時間鬧鐘(setitimer) 終止
SIGWINCH 終端窗口大小改變 忽略
SIGXCPU 超過CPU限制(setrlimit) 終止或終止+core
SIGXFSZ 超過文件長度限制(setrlimit) 終止或終止+core

下面詳細地說明一下個信號的含義:

  • SIGABRT 調用abort函數時產生此信號。進程異常終止。
  • SIGALRM 當alarm函數設置的定時器超時時,產生此信號。若由setitimer函數設置的間隔時間已經超時時,也產生此信號。
  • SIGBUS 指示一個實現定義的硬件故障。當出現某些類型的內存故障時,實現常常產生此種信號。
  • SIGCHLD 在一個進程終止或停止時,SIGCHLD信號被送給其父進程。按系統默認,將忽略此信號。如果父進程希望被告知其子進程的這種狀態改變,則應該捕捉此信號。
  • SIGCONT 此信號發送給需要繼續運行,但當前處於停止狀態的進程。如果當前進程處於停止狀態,則系統默認動作是使該進程繼續運行。
  • SIGEMT 指示一個實現定義的硬件故障。
  • SIGFPE 此信號表示一個算術運算異常,如除以0、浮點溢出等。
  • SIGHUP 如果終端接口檢測到一個連接斷開,則將此信號送給與該終端相關的控制進程(會話首進程)
  • SIGILL 此信號表示進程已執行一條非法硬件指令。
  • SIGITN 當用戶按中斷鍵,終端驅動程序產生此信號併發送至前臺進程組中的每一個進程。
  • SIGIO 此信號指示一個異步IO事件。
  • SIGIOT 指示一個實現定義的硬件故障。
  • SIGKILL 這是兩個不能被捕捉或忽略信號中的一個。它向系統管理員提供了一種可以殺死任一進程的可靠方法。
  • SIGPIPE 如果在管道進程的讀進程已終止時寫管道,則產生此信號。
  • SIGPWR 這是一種依賴於系統的信號。它主要應用於具有不間斷電源(UPS)的系統。如果電池電壓過低信息的進程將信號SIGPWR發送給init進程,然後由init處理停機操作。
  • SIGQUIT 類似於SIGINT信號的功能,但同時產生一個core文件。
  • SIGSEGV 指示進程進行了一次無效的內存引用(通常說明程序有錯誤,比如訪問了一個未接初始化的指針)。SEGV代表“段違例”(segmentation violation)。
  • SIGSTKFLT 這是早期有linux定義的,用於數學協處理器的棧故障。
  • SIGSTOP 這是一個作業控制信號,它停止一個進程。它不能被捕捉或忽略。
  • SIGSYS 該信號指示一個無效的系統調用。
  • SIGTERM 這是由kill命令發送的系統默認終止信號,這是由應用程序捕獲的,使用SIGTERM可以使程序有機會在退出前做好清理工作,從而優雅的終止(相對SIGKILL而言,SIGKILL不能被捕獲或忽略)。
  • SIGTRAP 指示一個實現定義的硬件故障。
  • SIGTSTP 交互停止信號,當用戶在終端上按掛起鍵(Ctrl+Z),終端驅動程序發出此信號,給前臺進程組中所有進程。
  • SIGTTIN 當一個後臺進程組試圖讀其控制終端時,終端驅動程序產生此信號。
  • SIGTTOU 當一個後臺進程組試圖寫其控制終端時,終端驅動程序產生此信號。
  • SIGGURG 此信號通知進程已經發生一個緊急情況,在網絡連接上接到帶外數據(帶外數據(out—of—band data),有時也稱爲加速數據(expedited data),是指連接雙方中的一方發生重要事情,想要迅速地通知對方。 )時,可選擇性地產生此信號。
  • SIGGUSR1 這是一個用戶定義的信號,可用於應用程序。
  • SIGGUSR2 這是一個用戶定義的信號,可用於應用程序。
  • SIGVTALRM 當一個有setitimer函數設置的虛擬間隔時間已經超時時,產生此信號。
  • SIGWINCH 內核維持每個終端或僞終端相關聯窗口的大小。進程可以用ioctl函數得到或設置窗口的大小。如果進程用ioctl的設置窗口大小命令更改了窗口大小,則內核將SIGWINCH信號發送至前臺進程組。
  • SIGXCPU 如果進程超過了其軟CPU時間限制,則產生此信號。
  • SIGXFSZ 如果進程超過了其軟文件長度限制,則產生此信號。

10.3 函數signal

UNIX系統信號機制最簡單的接口是signal函數。

#include <signal.h>
void (*signal (int signo,void(*func)(int)))(int);
//參數1:信號編號;參數2:信號處理函數指針;同時該函數返回之前該信號的信號處理函數

signal函數由ISO C定義。因爲ISO C不涉及多進程、進程組以及終端IO等,所有它對信號的定義非常含糊,以至於對UNIX系統而言幾乎毫無用處。
signo參數是信號名;
func的值是常量SIG_IGN、常量SIG_DFL或當連接到此信號後要調用的函數的地址。
SIG_IGN,向內核表示忽略此信號,有兩個信號不能忽略。
SIG_DFL,向內核表示接到此信號後的動作是系統默認動作。
當指定函數地址時,則在信號發生時,調用該函數,我們稱這種處理爲捕捉該信號,稱此函數爲信號處理程序(signal handler)或信號捕捉函數(signal-catching function)


之前的signal函數寫法比較複雜,可以簡化爲以下:

typedef void sigfunc(int);
sigfunc *signal(int ,sigfunc *);

如果查看signal.h頭文件可以看到:

#define SIG_ERR (void (*)())-1
#define SIG_DFL(void (*)())0
#define SIG_IGN(void (*)())1

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

static void sig_usr(int sign);           //信號處理函數

int main(void)
{
    if(signal(SIGUSR1,sig_usr)==SIG_ERR) //返回SIG_ERR說明系統不支持SIGUSR1
        printf("cant catch SIGUSR1 \n");
    if(signal(SIGUSR2,sig_usr)==SIG_ERR)
        printf("cant catch SIGUSR2 \n");
    while(1);//等待信號被捕獲
    return 0;
}

static void sig_usr(int signo)//信號處理函數
{
    if(signo==SIGUSR1)
        printf("received SIGUSR1\n");
    else if(signo==SIGUSR2)
        printf("received SIGUSR2\n");
    else
        printf("received signal %d \n",signo);

}

我們可以在後臺運行此程序,並使用kill命令向其發送信號:
這裏寫圖片描述

———-重點內容
1. 啓動程序
當執行一個程序時,所有信號的狀態都是系統默認或忽略。通常所有信號都被設置爲它們的默認動作,除非調用exec的進程忽略該信號。確切的講,exec函數將原先設置爲要捕捉的信號都更改爲默認動作,其他信號狀態則不變。(因爲原先進程的信號處理函數的地址在新的程序中已經沒有意義了。)
從signal函數可以看出,不更改信號的處理方式就不能確定信號的當前處理方式。在後面,我們將說明sigaction函數,可以確定一個函數的處理方式,而無需改變它。
2. 進程創建
當一個進程調用fork時,其子進程繼承父進程的信號處理方式。因爲子進程在開始時複製了父進程內存映像,所有信號捕捉函數的地址在子進程中是有意義的。

10.4 不可靠的信號

早期的某些實現,信號是不可靠的。

10.5 被中斷的系統調用

早期的UNIX系統的一個特性是:如果進程在執行一個低速系統調用而阻塞期間捕獲到一個信號,則該系統調用就被中斷不再繼續執行。該系統調用返回出錯,其errno設置爲EINTR。(這裏要分清系統調用和函數。當捕捉到某個信號時,被中斷的是內核中執行的系統調用。)
爲了支持這種特性,將系統調用分爲兩類:低速系統調用其他系統調用。低速系統調用是可能會使進程永遠阻塞的一類系統調用,包括:

  • 如果某些類型的文件的數據不存在,則讀操作可能會使調用者永遠阻塞;
  • 如果這些數據不能被相同的類型文件立即接受,則寫操作可能會使調用者永遠阻塞;
  • pause函數和wait函數;
  • 某些ioctl操作;
  • 某些進程間通信函數。
    可以使用中斷系統調用的一個例子是:如果已經進程啓動了讀終端操作,而該終端設備的用戶卻離開終端很長時間。這種情況下,進程可能被一直阻塞。

被中斷的系統調用必須顯式的處理出錯返回。例如:

again:
    if((n=fread(fd,buf,BUFSIZE)<0)){
    if(errno==EINTER)
    goto again;
    }

在系統調用被信號中斷後我們希望重新啓動它。
爲了幫助應用程序不必處理被中斷的系統調用,4.2BSD引進了某些被中斷
的系統調用自動重啓。自動重啓的系統調用包括:ioctl、read、readv、write、writev、wait和waitpid。
某些應用程序並不希望這些函數被中斷後重啓,爲此4.3BSD允許進程基於每個信號禁用此功能。

10.6 可重入函數

SUS說明了在信號處理程序中保證調用安全的函數。這些函數時可重入的並被稱爲是異步信號安全的(async-signal safe)。除了可重入外,在信號處理操作期間,它會阻塞任何會引起不一致信號的發送。異步信號安全的函數如下:

這裏寫圖片描述

以下形式的函數是不可重入的

  • 使用了靜態數據結構;
  • 調用了malloc或free;
  • 是標準IO函數;標準IO庫很多實現都以不可重入方式使用全局數據結構。雖然很多時候信號處理程序也調用了printf函數,但這並不保證產生所期望的結果,信號處理程序可能中斷主程序中的printf函數調用。
    注意:如果即使使用以上可重入函數,由於每個線程只有一個errno變量,所以信號處理程序可能會修改原先值。因此,作爲通用規則,應該在信號處理程序調用可重入函數之前保存errno值,再調用後恢復errno。

10.7 SIGCLD語義

SIGCLD和SIGCHLD這兩個信號很容易被混淆。

  • SIGCLD是System V的一個信號名。
  • SIGCHLD是BSD的信號名。
    BSD的SIGCHLD信號語義爲:子進程狀態改變後產生此信號,父進程需要調用一個wait函數已檢測發生了什麼。
    System V的就不說了。

10.8 可靠信號術語和語義

我們需要先定義一些在討論信號時會用到的術語。
首先,當造成信號的事件發生時,爲進程產生一個信號(或向進程發送一個信號)。事件可以是硬件異常(如除以0)、軟件條件(如alarm定時器超時)、終端產生的信號或調用kill函數。
當一個信號發生時,內核通常在進程表中以某種形式設置一個標誌。在信號產生(generation)和遞送(delivery)之間的時間間隔內,稱信號是未決的(pending)
進程可以選用“阻塞信號遞送”。如果爲進程產生了一個阻塞的信號,而且對該信號的動作是系統默認動作或捕捉該信號,則爲該進程將此信號保持爲未決狀態,直到該進程對此信號解除了阻塞,或者將對此信號的動作更改爲忽略。(如果信號是阻塞遞送的,則不調用處理函數,直到解除阻塞或忽略該信號)
內核在信號遞送時(而不是在信號產生時)決定對它的處理方式。於是進程在信號達到前仍可以改變對信號的動作。進程調用sigpending函數來判斷哪些信號是設置爲阻塞並處於未決狀態的。


如果系統實現支持POSIX.1實時擴展,那麼信號多次遞送給一個進程,就會排隊。如果不支持,那麼這些信號值遞送一次。

10.9 函數kill和raise

kill函數將信號發送給進程或進程組。
raise函數則允許進程向自身發送信號。

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

raise(signo)等價於kill(getpid(),signo);
kill的pid參數有以下4種不同的情況:

  • pid>0 將該信號發送給進程ID爲pid的進程。
  • pid==0 將該信號發送給與發送進程屬於同一進程組的所有進程(這些進程 的進程組ID等於發送進程的進程組ID),而且發送進程具有權限向這些進程發送信號。
  • pid<0 將該信號發送給其進程組ID等於pid絕對值,而且發送進程具有權限向其發送信號的所有進程。
  • pid==-1 將該信號發送給發送進程有權限向他們發送信號的所以進程。

10.10 函數alarm和pause

使用alarm函數可以設置一個定時器(鬧鐘時間),在將來的某個時刻定時器會超時,當定時器超時時,產生SIGALRM信號。如果忽略或不捕捉此信號,則其默認動作是終止調用該alarm函數的進程。

#include <unistd.h>
unsigend int alarm(unsigend int seconds);//返回上次alarm未超時的剩餘時間。如果second爲0,則取消上次爲超時的鬧鐘。

pause函數使調用進程掛起直至捕捉到一個信號。

#include <unistd.h>
int pause(void);

只有執行了一個信號處理程序並從其返回時,pause才返回。在這種情況下,pause返回-1,errno設置爲EINTR.

10.11 信號集

我們需要一個能表示多個信號-信號集(signal set)的數據類型。我們將在sigprocmask類函數中使用這種數據類型,以便告訴內核不允許發生該信號集中的信號
POSIX.1 定義數據類型sigset_t以包含一個信號集,並定義了下列5個處理信號集的函數:

#include <signal.h>

int sigemptyset(sigset_t *set); //初始化set,清除所有信號。必須初始化一次,以屏蔽不同系統之間的差異。
int sigfillset(sigset_t *set); //初始化set,添加所有信號。

int sigaddset(sigset_t *set,int signo); //添加一個指定的信號。
int sigdelset(sigset_t *set,int signo); //刪除一個指定的信號。
int sigismember(const sigset_t *set ,int signo); //判斷一個信號是否在信號集中。

10.12 函數sigprocmask

一個進程的信號屏蔽字規定了當前阻塞而不能遞送給該進程信號集
函數sigprocmask函數可以檢測或更改,或同時進行檢測和更改進程的信號屏蔽字:

#include <signal.h>

int sigprocmask(int how,const sigset_t *restrict set,sigset_t *restrict oset);
  • 若oset是非空指針,那麼進程的當前信號屏蔽字通過oset返回。
  • 若set是一個非空指針,則參數how指示如何修改當前信號屏蔽字。
  • 如果set是個空指針,則不改變進程的信號屏蔽字,how的值也無意義。
how 說明
SIG_BLOCK 該進程新的信號屏蔽字是當前信號屏蔽字和set指向信號集的並集。set包含了希望阻塞的附加信號
SIG_UNBLOCK 該進程新的信號屏蔽字是當前信號屏蔽字和set指向信號集的補集的交集。set包含了希望解除阻塞的附加信號
SIG_SETMASK 該進程新的信號屏蔽是set指向的值

在調用sigprocmask後如果有任何未決的、不再阻塞的信號,則在sigprocmask返回前,至少將其中之一遞送給該進程。
sigprocmask是僅爲單線程進程定義的。多線程中信號的屏蔽使用另一個函數。

10.13 函數sigpending

sigpending函數返回一信號集,對於調用進程而言,其中各信號是阻塞不能遞送的,因而也一定是當前未決的。該信號集通過set參數返回。

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

 static void sig_quit()
 {
     printf("caught SIGQUIT. \n");
     if(signal(SIGQUIT,SIG_DFL)==SIG_ERR)
     printf("cant reset SIGQUIT. \n");
 }

 int main(void)
 {
     sigset newmask, oldmask, pendmask;
     if(signal(SIGQUIT,sig_quit)==SIG_ERR)//註冊SIGQUIT信號處理函數
        printf("cant catch SIGQUIT. \n");    
    /*
    阻塞SIGQUIT信號並保存當前信號屏蔽字
    */
    sigemptyset(&newmansk);  //初始化newmask信號集
    sigaddset(&newmansk,SIGQUIT);  //新增SIGQUIT信號
    if(sigprocmask(SET_BLOCK,&newmask,&oldmask)<0)  //阻塞newmask信號集
        printf("SIG_BLOCK error. \n");
    sleep(5);
    if(sigpending(&pendingmask)<0)  //讀取當前未遞送信號
     printf("pending error. \n");    
    if(sigismember(&pendmaks,SIGQUIT))  //判斷當前未遞送信號是否包括SIGQUIT信號
     printf("SIGQUIT pending. \n"); 
    /*
    恢復沒有屏蔽SIGQUIT的信號屏蔽字
    */
    if(sigprocmask(SET_SETMASK,&oldmansk,NULL)<0) //恢復信號屏蔽字設置
        printf("SIG_SETMAKS error. \n");     
    printf("SIGQUIT unblocked. \n");
    sleep(5);
    exit(0);
 }

我們執行此程序:

$ ./a.out
^\          //產生信號一次(5s內)
SIGQUIT pending     //從sleep返回後
caught SIGQUIT      //在信號處理程序中捕獲(sigprocmask(SET_SETMASK,&oldmansk,NULL)返回前)
SIGQUIT unblocked   //從sigprocmask返回後
^\Quit(coredump)    //再次產生信號

$ ./a.out
^\^\^\^\^\^\        //產生多次信號(5s內)
SIGQUIT pending     //從sleep返回後
caught SIGQUIT      //在信號處理程序中捕獲(sigprocmask(SET_SETMASK,&oldmansk,NULL)返回前)
SIGQUIT unblocked   //從sigprocmask返回後
^\Quit(coredump)    //再次產生信號

shell發現其子進程異常終止時輸出QUIT(coredump)信息。在第二次運行程序時,在休眠期間使SIGQUIT信號產生了多次,但解除對該信號的阻塞後,只向進程傳遞一次SIGQUIT信號。從中可以看到在此係統上沒有將信號進行排隊。

10.14 函數sigaction

sigaction函數的功能是檢查或修改(或檢查並修改)與指定信號相關聯的處理動作。此函數取代了UNIX早期版本使用的signal函數。在本節末尾使用sigaction函數實現了signal。

#include <signal.h>
int sigaction(int signo,const struct sigaction *restrict act,struct sigaction *restrict oact);
  • 其中參數signo是要檢測或修改具體動作的信號編號。
  • 若act非空指針,則要修改其動作。
  • 若oact非空指針,則系統經由oact指針返回該信號的上一個動作。
struct sigaction{
    void (*sa_handler)(int);//信號處理函數,或SIG_IGN或SIG_DFL
    sigset_t sa_mask;//信號集屏蔽字
    int sa_flags;//信號選項
    void (*sa_sigaction)(int,siginfo_t *,void *);
};

當更改信號動作時,如果sa_handler字段包括一個信號捕捉函數的地址(不是常量SIG_IGN或SIG_DFL),則sa_mask字段說明了一個信號集,在調用該信號捕捉函數之前,這一信號集要加到進程的信號屏蔽字中。僅當從信號捕捉函數返回時再將進程的信號屏蔽字恢復爲原先值。
這樣在調用信號處理程序時就能阻塞某些信號。在信號處理程序被調用時,操作系統建立的新信號屏蔽字包括正被遞送的信號。因此保證了在處理一個給定信號時,如果這種信號再次發生,那麼它會被阻塞到對前一個信號處理結束爲止。
若同一信號多次發生,通常並不將它們加入隊列,所以如果在某種信號被阻塞時,這種信號發生了多次,那麼對這種信號解除阻塞後,其信號處理函數通常只會被調用一次。

act結構的sa_flag字段指定對信號進行處理的各個選項。以下列出了這些選項的意義:

選項 說明
SA_INTERRUPT 由此信號中斷的系統調用不自動重啓動。
SA_NOCLDSTOP 若signo是SIGCHLD,當子進程停止,不產生此信號;當子進程停止後繼續運行,不產生此信號;當子進程終止時產生此信號。
SA_NOCLDWAIT 若signo是SIGCHLD,當調用進程的子進程終止時,不創建僵死進程。若調用進程隨後調用wait,則阻塞到它所有子進程都終止,此時返回-1,errno設置爲ECHILD。
SA_NODEFER 當捕捉到此信號時,在執行器信號捕捉函數時,系統不自動阻塞此信號(除非sa_mask包括了此信號)
SA_NOSTACK 若signalstack已聲明瞭一個替換棧,則此信號遞送給替換棧上的進程。(沒懂)
SA_RESETHAND 在此信號捕捉函數的入口處,將此信號的處理方式重置爲SIG_DFL,並清除SA_SIGINFO標誌。
SA_RESTART 被此信號中斷的系統調用自動重啓
SA_SIGINFO 此選項對信號處理程序提供了附加信息:一個指向siginfo結構的指針以及一個指向進程上下文標識符的指針。

sa_sigaction字段是一個替代的信號處理程序,在sigaction結構中使用了SA_SIGINFO標誌時,使用該信號處理程序。
若使用了SA_SIGINFO標誌,那麼按這種方式調用信號處理程序:void (*sa_sigaction)(int,siginfo_t *,void *);
這裏的siginfo結構包含了信號產生原因的有關信息。

struct siginfo{
int si_signo;  //信號編號
int si_erron;  //包含錯誤編號,它對應於造成信號產生的條件
int si_code;   //附加信息
pid_t si_pid;  //發送的進程ID
uid_t si_uid;  //發送的用戶ID
void *si_addr; //導致故障的根源地址,該地址可能不準
int si_status; //退出號或信號編號
union sigval si_value;
};

sigval聯合包含以下字段:

int sival_int;
void *sival_ptr;

應用程序在傳遞信號時,在si_value.sival_int中傳遞一個整型數或在si_value.sival_ptr中傳遞一個指針值。
(這一節不太懂)

10.15 函數sigsetjmp和siglognjmp

7.10節說明了用於非局部轉移的setjmp和longjmp函數。
在信號處理程序中經常調用longjmp函數以返回到程序的主循環中,而不是從該程序返回。但是longjmp有一個問題。當捕捉到一個信號時,進入信號捕捉函數,此時當前信號被自動地加到進程的信號屏蔽字中。這阻止了後來產生的這種信號中斷該信號處理程序。如果在信號處理程序中使用longjmp跳出信號處理程序,那麼對此進程的信號屏蔽字會發生什麼,不同的實現處理方法並不相同。
因此POSIX.1並沒有指定setjmp和longjmp對信號屏蔽字的作用,而是定義了兩新函數sigsetjmp和siglongjmp。在信號處理程序中進行非局部轉移時應當使用這兩個函數。

#include <setjmp.h>
int simsetjmp(sigjmp_buf env,int savemask);//直接調用返回0;從siglongjmp調用返回非0
void siglongjmp(sigjmp_buf env,int val);

這兩個函數和setjmp、longjmp之間的唯一區別是sigsetjmp增加了一個參數。如果savemask非0,則sigsetjmp在env中保存當前信號屏蔽字。掉siglongjmp時,如果帶非0 sacemask的sigsetjmp調用已經保存了env,則siglongjmp從其中恢復保存的信號屏蔽字。
當調用一個信號處理程序時,被捕捉到的信號加到進程的當前信號屏蔽字中。當從信號處理程序返回時,返回原來的屏蔽字。

10.16 函數sigsuspend

上面已經說明,更改進程的信號屏蔽字可以阻塞所選擇的信號,或解除對它們的阻塞。使用這種技術可以保護不希望由信號中斷的代碼臨界區。
函數sigsuspend可以掛起進程,直到其信號屏蔽字之外的信號被捕獲並從其信號處理程序返回。返回之後系統的信號屏蔽字恢復爲調用sigsuspend之前的信號屏蔽字。

#include <signal.h>
int sigsuspend(const sigset_t *sigmask);
#include <stdio.h>
#include <signal.h>

static void sig_int(int sign)
{
    printf("in sig_int handler. \n");
}

static void sig_usr1(int sign)
{
    printf("in sig_usr1 handler. \n");
}


int main(void)
{

    sigset_t newmask,oldmask,waitmask;
    printf("program start.\n");

    if(signal(SIGINT,sig_int)==SIG_ERR) printf("signal err. \n");
    if(signal(SIGUSR1,sig_usr1)==SIG_ERR) printf("signal err. \n");

    sigemptyset(&waitmask);
    sigaddset(&waitmask,SIGUSR1);   //將SIGUSR1信號添加到信號屏蔽字waitmask

    sigemptyset(&newmask);
    sigaddset(&newmask,SIGINT);     //將SIGINT信號添加到信號屏蔽字newmask

//阻塞屏蔽字中的信號,此後屏蔽了信號SIGUSR1
    if(sigprocmask(SIG_BLOCK,&waitmask,&oldmask)<0) printf("SIG_BLOCK err. \n");

   //掛起進程,直到屏蔽字以外的信號被捕獲到並從其信號處理程序返回後,進程繼續運行
    if(sigsuspend(&newmask)!= -1) printf("sigsuspend error. \n");
    printf("after return sigsuspend . \n");
  //此時阻塞了信號SIGINT
    pause();

    return 0;
}

這裏寫圖片描述


sigsuspend的另一種應用是等待一個信號處理程序設置一個全局變量。例如:

static volatile sig_atmoic_t sigflag;

int sig_proc(int signo)
{
    if(signo==SIGINT)
        printf("interrupt. \n");
    else if(signo==SIGQUIT)
        sigflag=1;
}

int main(void)
{
    sigset_t newmask,oldmask,zeromask;
    if(signal(SIGINT,sig_proc)==SIGERR)printf("sig error.\n");
    if(signalSIGQUIT,sig_procSIGERR)printf("sig error.\n");
//intilization sigset 
sigemptyset(&zeromask);
sigemptyset(&newmask);
sigaddset(&newmask,SIGQUIT);

//block SIGQUIT and save current signal mask
if(sigprocmask(SIG_BLOCK,&newmask,&oldmask)<0) printf("sigprocmask error. \n");
//sigsuspend zeromask and release SIG_QUIT
    while(sigflag==0)//這裏會一直循環,直到sigflag=1。也就是SIGQUIT信號,程序繼續往下走。
        sigsuspend(&zeromask);
    sigflag=0;
if(sigprocmask(SIG_SETMASK,&oldmaks,NULL)<0) printf("sigprocmask error. \n");
}

10.17 函數abort

前面已經提及abort函數的功能是使程序異常終止。

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

此函數將SIGABRT信號發送給調用進程(不應忽略此信號)。調用abort將向主機環境遞送一個未成功終止的通知,其方法是raise(SIGABRT)函數。
ISO C要求若捕捉到此信號而且相應信號處理程序返回,abort仍不會返回到其調用者。如果捕捉到此信號,則信號處理程序不能返回的唯一方法是它調用exit、_exit、longjmp或siglongjmp。POSIX.1也說明abort並不理會進程對此信號的阻塞和忽略。
讓進程捕捉SIGABRT的意圖是:在進程終止之前由其執行所需的清理操作。如果進程並不在信號處理程序中終止自己,POSIX.1聲明當信號處理程序返回時,abort終止該線程。
ISO C針對此函數的規範將是否沖洗數據留個實現決定。
POSIX.1則要求當調用abort終止進程前,則它對所有打開的標準IO流執行沖洗操作。

10.18 函數system

POSIX.1要求system忽略SIGINT和SIGQUIT,阻塞SIGCHLD.


ed編輯器不熟悉,看的暈暈的。以後再說。

10.19 函數sleep、nanosleep和clock_nanosleep

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

此函數使調用進程掛起直到滿足下面兩個條件之一:

  • 已經經過了seconds所指定的牆上時鐘時間。
  • 調用進程捕捉到一個信號並從信號處理程序返回。
    如同alarm函數一樣,由於其他系統活動,實際返回時間比所要求的會遲一些。
    在第一種情形下,返回值是0;
    在第二種情形下,由於捕捉到某個信號sleep提早返回時,返回值是未休眠完的秒數(所要求的時間減去實際休眠時間)。

nanosleep函數與sleep函數類似,但提供了納秒級的精度。

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

這個函數調用掛起進程,直到要求的時間已經超時或者某個信號中斷了該函數。
reqtp參數用秒和納秒指定了需要休眠的時間長度。
remtp參數指向的timespec結構就會被設置爲未休眠完的時間長度。如果對未休眠完的時間並不感興趣,可以把該參數設置爲NULL
如果系統並不支持納秒這一精度,要求的時間就會取整。因爲nanosleep函數並不涉及產生任何信號,所以不需要擔心與其他函數的交互。


隨着多個系統時鐘的引入,需要使用相對於特定時鐘的延時時間來掛起調用線程clock_nanosleep函數提供了這種功能。

#include <time.h>
int clock_nanosleep(clockid_t clock_id,int flags,const struct timespec *reqtp,struct timespec *remtp);
  • clock_id參數指定了計算延時時間基於的時鐘;
  • flag參數用於控制延遲是相對的還是絕對的,flags爲0:表示休眠時間是相對的。如果flags是TIMER_ABSTIME:表示休眠時間是絕對的,希望休眠到某個特定的時間。
  • reqtp和remtp參數和nanosleep中的相同。
    除了出錯返回,調用clock_nanosleep(CLOCK_REALTIME,0,reqtp,remtp);nanosleep(reqtp,remtp);作用是相同的。

使用絕對時間改善了延時精度。因爲相對時間取決於系統調度。

10.20 函數sigqueue

有些系統開始增加對信號排隊的支持。除了信號排隊以外,這些擴展允許應用程序在遞交信號時傳遞更多的信息。這些信息嵌入在siginfo結構中。 除了系統提供的信息,應用程序還可以向信號處理程序傳遞整數或者包含更多信息的緩衝區指針。
使用排隊信號必須做以下幾個操作:

  • 使用sigaction函數安裝信號處理程序時指定SA_SIGINFO標誌。如果沒有給出這個標誌,信號會延遲,但信號是否進入隊列要取決於具體實現。
  • 在sigaction結構的sa_sigaction成員中提供信號處理程序。
  • 使用sigqueue函數發送信號。
#include <signal.h>
int sigqueue(pid_t pid,int signo,const union sigval value);

sigque函數只能把信號發送給單個進程,可以使用value參數向信號處理程序傳遞整數和指針,除此之外,sigqueue函數與kill函數類似。

10.21 作業控制信號

所有的信號中,POSIX.1認爲有以下6個與作業控制有關。

  • SIGCHLD 子進程已經停止或終止。
  • SIGCONT 如果進程已經停止,則使其繼續運行。
  • SIGSTOP 停止信號(不能被捕捉或忽略)。
  • SIGTSTP 交互式停止信號。
  • SIGTTIN 後臺進程組成員讀控制終端。
  • SIGTTOU 後臺進程組成員寫控制終端。
    出了SIGCHLD以外,大多數應用程序並不處理這些信號,交互式shell通常會處理這些信號的所有工作

  • 當鍵入掛起字符(Ctrl+Z)時,SIGTSTP信號被送至前臺進程組的所有進程。

  • 當我們通知shell在前臺或後臺恢復一個作業時,shell向該作業中所有進程發送SIGCONT信號。
  • 與此類似,如果一個進程遞送了SIGTTIN或SIGTTOU信號,則根據系統默認的方式,停止此進程,作業控制shell瞭解到這一點後就通知我們。

10.22 信號名和編號

本節介紹如何在信號編號和信號名之間進行映射。某些系統提供數組
extern char *sys_siglist[];
數組下標是信號編號,數組中的元素是指向信號名字符串的指針。
可以使用psignal函數打印與信號編號對應的字符串。

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

如果在sigaction信號處理程序中有siginfo結構,可以使用psiginfo函數打印信號信息。

#include <string.h>
void *strsignal(int signp); //說明描述信號的字符串是全局變量。

給出一個信號編號,strsignal將返回描述該信號的字符串。引用程序可以用該字符串打印關於接收到信號的出錯消息。

10.23 小結

信號用於大多數複雜的應用程序中。理解進行信號處理的原因和方式對於高級UNIX編程極爲重要。
本章首先說明了早起信號實現的問題以及它們是如何顯現出來的

  • 介紹了POSIX.1的可靠信號概念以及所有相關的信號函數
  • 在此基礎上提供了abort、system和sleep函數的實現
  • 最後以觀察分析作業控制信號以及信號名和信號編號之間的轉換結束。
發佈了55 篇原創文章 · 獲贊 13 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章