天天看點

linux 信号詳解 一、信号的基本概念 

linux下的程序間通信及線程間同步,都用過了,就是信号程式設計沒用過,也不知道在什麼情況下會使用信号程式設計,研究一下

        信号機制是程序之間互相傳遞消息的一種方法,信号全稱為軟中斷信号,也有人稱作軟中斷。從它的命名可以看出,它的實質和使用很象中斷。是以,信号可以說是程序控制的一部分。

一、信号的基本概念 

1、基本概念 

軟中斷信号(signal,又簡稱為信号)用來通知程序發生了異步事件。程序之間可以互相通過系統調用kill發送軟中斷信号。核心也可以因為内部事件而給程序發送信号,通知程序發生了某個事件。注意,信号隻是用來通知某程序發生了什麼事件,并不給該程序傳遞任何資料。

收到信号的程序對各種信号有不同的處理方法。處理方法可以分為三類:

第一種是類似中斷的處理程式,對于需要處理的信号,程序可以指定處理函數,由該函數來處 理。 第二種方法是,忽略某個信号,對該信号不做任何處理,就象未發生過一樣。 第三種方法是,對該信号的處理保留系統的預設值,這種預設操作,對大部分的信 号的預設操作是使得程序終止。   程序通過系統調用signal來指定程序對某個信号的處理行為。

在程序表的表項中有一個軟中斷信号域,該域中每一位對應一個信号,當有信号發送給程序時,對應位置位。由此可以看出,程序對不同的信号可以同時保留,但對于同一個信号,程序并不知道在處理之前來過多少個。

2、信号的類型 

Linux支援的信号清單如下。很多信号是與機器的體系結構相關的,首先列出的是POSIX.1中列出的信号:

信号 值 處理動作 發出信号的原因

---------------------------------------------------------------------

SIGHUP 1 A 終端挂起或者控制程序終止

SIGINT 2 A 鍵盤中斷(如break鍵被按下)

SIGQUIT 3 C 鍵盤的退出鍵被按下

SIGILL 4 C 非法指令

SIGABRT 6 C 由abort(3)發出的退出指令

SIGFPE 8 C 浮點異常

SIGKILL 9 AEF Kill信号

SIGSEGV 11 C 無效的記憶體引用

SIGPIPE 13 A 管道破裂: 寫一個沒有讀端口的管道

SIGALRM 14 A 由alarm(2)發出的信号

SIGTERM 15 A 終止信号

SIGUSR1 30,10,16 A 使用者自定義信号1

SIGUSR2 31,12,17 A 使用者自定義信号2

SIGCHLD 20,17,18 B 子程序結束信号

SIGCONT 19,18,25 程序繼續(曾被停止的程序)

SIGSTOP 17,19,23 DEF 終止程序

SIGTSTP 18,20,24 D 控制終端(tty)上按下停止鍵

SIGTTIN 21,21,26 D 背景程序企圖從控制終端讀

SIGTTOU 22,22,27 D 背景程序企圖從控制終端寫

下面的信号沒在POSIX.1中列出,而在SUSv2列出 

信号 值 處理動作 發出信号的原因

--------------------------------------------------------------------

SIGBUS 10,7,10 C 總線錯誤(錯誤的記憶體通路)

SIGPOLL A Sys V定義的Pollable事件,與SIGIO同義

SIGPROF 27,27,29 A Profiling定時器到

SIGSYS 12,-,12 C 無效的系統調用 (SVID)

SIGTRAP 5 C 跟蹤/斷點捕獲

SIGURG 16,23,21 B Socket出現緊急條件(4.2 BSD)

SIGVTALRM 26,26,28 A 實際時間報警時鐘信号(4.2 BSD)

SIGXCPU 24,24,30 C 超出設定的CPU時間限制(4.2 BSD)

SIGXFSZ 25,25,31 C 超出設定的檔案大小限制(4.2 BSD)

(對于SIGSYS,SIGXCPU,SIGXFSZ,以及某些機器體系結構下的SIGBUS,Linux預設的動作是A (terminate),SUSv2 是C (terminate and dump core))。

下面是其它的一些信号

信号 值 處理動作 發出信号的原因

---------------------------------------------------------------------

SIGIOT 6 C IO捕獲指令,與SIGABRT同義

SIGEMT 7,-,7

SIGSTKFLT -,16,- A 協處理器堆棧錯誤

SIGIO 23,29,22 A 某I/O操作現在可以進行了(4.2 BSD)

SIGCLD -,-,18 A 與SIGCHLD同義

SIGPWR 29,30,19 A 電源故障(System V)

SIGINFO 29,-,- A 與SIGPWR同義

SIGLOST -,-,- A 檔案鎖丢失

SIGWINCH 28,28,20 B 視窗大小改變(4.3 BSD, Sun)

SIGUNUSED -,31,- A 未使用的信号(will be SIGSYS)

(在這裡,- 表示信号沒有實作;有三個值給出的含義為,第一個值通常在Alpha和Sparc上有效,中間的值對應i386和ppc以及sh,最後一個值對應mips。信号29在Alpha上為SIGINFO / SIGPWR ,在Sparc上為SIGLOST。)

處理動作一項中的字母含義如下

A 預設的動作是終止程序

B 預設的動作是忽略此信号

C 預設的動作是終止程序并進行核心映像轉儲(dump core)

D 預設的動作是停止程序

E 信号不能被捕獲

F 信号不能被忽略

上面介紹的信号是常見系統所支援的。以表格的形式介紹了各種信号的名稱、作用及其在預設情況下的處理動作。各種預設處理動作的含義是:

終止程式是指程序退 出;忽略該信号是将該信号丢棄,不做處理; 停止程式是指程式挂起,進入停止狀況以後還能重新進行下去,一般是在調試的過程中(例如ptrace系統調 用); 核心映像轉儲是指将程序資料在記憶體的映像和程序在核心結構中存儲的部分内容以一定格式轉儲到檔案系統,并且程序退出執行,這樣做的好處是為程式員提供了友善,使得他們可以得到程序當時執行時的資料值,允許他們确定轉儲的原因,并且可以調試他們的程式。

注意信号SIGKILL和SIGSTOP既不能被捕捉,也不能被忽略。信号SIGIOT與SIGABRT是一個信号。可以看出,同一個信号在不同的系統中值可能不一樣,是以建議最好使用為信号定義的名字,而不要直接使用信号的值。

二、信 号 機 制

上 一節中介紹了信号的基本概念,在這一節中,我們将介紹核心如何實作信号機制。即核心如何向一個程序發送信号、程序如何接收一個信号、程序怎樣控制自己對信 号的反應、核心在什麼時機處理和怎樣處理程序收到的信号。還要介紹一下setjmp和longjmp在信号中起到的作用。 

1、核心對信号的基本處理方法

内 核給一個程序發送軟中斷信号的方法,是在程序所在的程序表項的信号域設定對應于該信号的位。這裡要補充的是,如果信号發送給一個正在睡眠的程序,那麼要看 該程序進入睡眠的優先級,如果程序睡眠在可被中斷的優先級上,則喚醒程序;否則僅設定程序表中信号域相應的位,而不喚醒程序。這一點比較重要,因為程序檢 查是否收到信号的時機是:一個程序在即将從核心态傳回到使用者态時;或者,在一個程序要進入或離開一個适當的低排程優先級睡眠狀态時。

核心處理一個程序收到的信号的時機是在一個程序從核心态傳回使用者态時。是以,當一個程序在核心态下運作時,軟中斷信号并不立即起作用,要等到将傳回使用者态時才處理。程序隻有處理完信号才會傳回使用者态,程序在使用者态下不會有未處理完的信号.

内 核處理一個程序收到的軟中斷信号是在該程序的上下文中,是以,程序必須處于運作狀态。前面介紹概念的時候講過,處理信号有三種類型:程序接收到信号後退 出;程序忽略該信号;程序收到信号後執行使用者設定用系統調用signal的函數。當程序接收到一個它忽略的信号時,程序丢棄該信号,就象沒有收到該信号似 的繼續運作。如果程序收到一個要捕捉的信号,那麼程序從核心态傳回使用者态時執行使用者定義的函數。而且執行使用者定義的函數的方法很巧妙,核心是在使用者棧上創 建一個新的層,該層中将傳回位址的值設定成使用者定義的處理函數的位址,這樣程序從核心傳回彈出棧頂時就傳回到使用者定義的函數處,從函數傳回再彈出棧頂時, 才傳回原先進入核心的地方。這樣做的原因是使用者定義的處理函數不能且不允許在核心态下執行(如果使用者定義的函數在核心态下運作的話,使用者就可以獲得任何權 限)。 

在信号的處理方法中有幾點特别要引起注意:

  第一,在一些系統中,當一個程序處理完中斷信号傳回使用者态之前, 核心清除使用者區中設 定的對該信号的處理例程的位址,即下一次程序對該信号的處理方法又改為預設值,除非在下一次信号到來之前再次使用signal系統調用。這可能會使得程序 在調用signal之前又得到該信号而導緻退出。在BSD中,核心不再清除該位址。但不清除該位址可能使得程序因為過多過快的得到某個信号而導緻堆棧溢 出。為了避免出現上述情況。在BSD系統中,核心模拟了對硬體中斷的處理方法,即在處理某個中斷時,阻止接收新的該類中斷。

第二個要 引起注意的是,如果要捕捉的信号發生于程序正在一個系統調用中時,并且該程序睡眠在可中斷的優先級上,這時該信号引起程序作一次longjmp,跳出睡眠 狀态,傳回使用者态并執行信号處理例程。當從信号處理例程傳回時,程序就象從系統調用傳回一樣,但傳回了一個錯誤代碼,指出該次系統調用曾經被中斷。這要注 意的是,BSD系統中核心可以自動地重新開始系統調用。

第三個要注意的地方:若程序睡眠在可中斷的優先級上,則當它收到一個要忽略的信号時,該程序被喚醒,但不做longjmp,一般是繼續睡眠。但使用者感覺不到程序曾經被喚醒,而是象沒有發生過該信号一樣。

第 四個要注意的地方:核心對子程序終止(SIGCLD)信号的處理方法與其他信号有所差別。當程序檢查出收到了一個子程序終止的信号時,預設情況下,該程序 就象沒有收到該信号似的,如果父程序執行了系統調用wait,程序将從系統調用wait中醒來并傳回wait調用,執行一系列wait調用的後續操作(找 出僵死的子程序,釋放子程序的程序表項),然後從wait中傳回。SIGCLD信号的作用是喚醒一個睡眠在可被中斷優先級上的程序。如果該程序捕捉了這個 信号,就象普通信号處理一樣轉到處理例程。如果程序忽略該信号,那麼系統調用wait的動作就有所不同,因為SIGCLD的作用僅僅是喚醒一個睡眠在可被 中斷優先級上的程序,那麼執行wait調用的父程序被喚醒繼續執行wait調用的後續操作,然後等待其他的子程序。

如果一個程序調用signal系統調用,并設定了SIGCLD的處理方法,并且該程序有子程序處于僵死狀态,則核心将向該程序發一個SIGCLD信号。 

2、setjmp和longjmp的作用

前面在介紹信号處理機制時,多次提到了setjmp和longjmp,但沒有仔細說明它們的作用和實作方法。這裡就此作一個簡單的介紹。

在 介紹信号的時候,我們看到多個地方要求程序在檢查收到信号後,從原來的系統調用中直接傳回,而不是等到該調用完成。這種程序突然改變其上下文的情況,就是 使用setjmp和longjmp的結果。setjmp将儲存的上下文存入使用者區,并繼續在舊的上下文中執行。這就是說,程序執行一個系統調用,當因為資 源或其他原因要去睡眠時,核心為程序作了一次setjmp,如果在睡眠中被信号喚醒,程序不能再進入睡眠時,核心為程序調用longjmp,該操作是核心 為程序将原先setjmp調用儲存在程序使用者區的上下文恢複成現在的上下文,這樣就使得程序可以恢複等待資源前的狀态,而且核心為setjmp傳回1,使 得程序知道該次系統調用失敗。這就是它們的作用。

三、有關信号的系統調用

前面兩節已經介紹了有關信号的大部分知 識。這一節我們來了解一下這些系統調用。其中,系統調用signal是程序用來設定某個信号的處理方法,系統調用kill是用來發送信号給指定程序的。這 兩個調用可以形成信号的基本操作。後兩個調用pause和alarm是通過信号實作的程序暫停和定時器,調用alarm是通過信号通知程序定時器到時。所 以在這裡,我們還要介紹這兩個調用。

1、signal 系統調用

系統調用signal用來設定某個信号的處理方法。該調用聲明的格式如下:

void (*signal(int signum, void (*handler)(int)))(int);

在使用該調用的程序中加入以下頭檔案:

#include <signal.h>

上述聲明格式比較複雜,如果不清楚如何使用,也可以通過下面這種類型定義的格式來使用(POSIX的定義):

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

但這種格式在不同的系統中有不同的類型定義,是以要使用這種格式,最好還是參考一下聯機手冊。

在調用中,參數signum指出要設定處理方法的信号。第二個參數handler是一個處理函數,或者是

SIG_IGN:忽略參數signum所指的信号。

SIG_DFL:恢複參數signum所指信号的處理方法為預設值。

傳遞給信号處理例程的整數參數是信号值,這樣可以使得一個信号處理例程處理多個信号。系統調用signal傳回值是指定信号signum前一次的處理例程或者錯誤時傳回錯誤代碼SIG_ERR。下面來看一個簡單的例子:

#include <signal.h>

#include <unistd.h>

#include <stdio.h>

void sigroutine(int dunno) {

switch (dunno) {

case 1:

printf("Get a signal -- SIGHUP ");

break;

case 2:

printf("Get a signal -- SIGINT ");

break;

case 3:

printf("Get a signal -- SIGQUIT ");

break;

}

return;

}

int main() {

printf("process id is %d ",getpid());

signal(SIGHUP, sigroutine); /

struct timeval it_value;

};

該結構中timeval結構定義如下:

struct timeval {

long tv_sec;

long tv_usec;

};

在setitimer 調用中,參數ovalue如果不為空,則其中保留的是上次調用設定的值。定時器将it_value遞減到0時,産生一個信号,并将it_value的值設 定為it_interval的值,然後重新開始計時,如此往複。當it_value設定為0時,計時器停止,或者當它計時到期,而it_interval 為0時停止。調用成功時,傳回0;錯誤時,傳回-1,并設定相應的錯誤代碼errno:

EFAULT:參數value或ovalue是無效的指針。

EINVAL:參數which不是ITIMER_REAL、ITIMER_VIRT或ITIMER_PROF中的一個。

下面是關于setitimer調用的一個簡單示範,在該例子中,每隔一秒發出一個SIGALRM,每隔0.5秒發出一個SIGVTALRM信号:

#include <signal.h>

#include <unistd.h>

#include <stdio.h>

#include <sys/time.h>

int sec;

void sigroutine(int signo) {

switch (signo) {

case SIGALRM:

printf("Catch a signal -- SIGALRM ");

break;

case SIGVTALRM:

printf("Catch a signal -- SIGVTALRM ");

break;

}

return;

}

int main() {

struct itimerval value,ovalue,value2;

sec = 5;

printf("process id is %d ",getpid());

signal(SIGALRM, sigroutine);

signal(SIGVTALRM, sigroutine);

value.it_value.tv_sec = 1;

value.it_value.tv_usec = 0;

value.it_interval.tv_sec = 1;

value.it_interval.tv_usec = 0;

setitimer(ITIMER_REAL, &value, &ovalue);

value2.it_value.tv_sec = 0;

value2.it_value.tv_usec = 500000;

value2.it_interval.tv_sec = 0;

value2.it_interval.tv_usec = 500000;

setitimer(ITIMER_VIRTUAL, &value2, &ovalue);

for (;;) ;

}

該例子的螢幕拷貝如下:

localhost:~$ ./timer_test

process id is 579

Catch a signal – SIGVTALRM

Catch a signal – SIGALRM

Catch a signal – SIGVTALRM

Catch a signal – SIGVTALRM

Catch a signal – SIGALRM

Catch a signal –GVTALRM

以下将剖析 ANSI <signal.h>庫并示範如何使用其接口。進而讨論 POSIX 信号處理 API。

  信号處理類似硬體中斷。它們促使某個程序從目前的執行控制流程中跳出,以實作特定的行為,待特定處理完成後,再恢複到中斷點繼續執行。本文将剖析 ANSI <signal.h>庫并示範如何使用其接口。然後,本文将進而讨論POSIX 信号處理 API。 預設情況下,某些信号導緻程序終止。例如,試圖存取程序不擁有的記憶體将觸發 SIGSEGV (“段故障”)信号,這時該信号會終止程序的執行。許多應用程式都有這個問題,這是我們不希望看到的。調試,仿真和事務處理系統必須處理這樣的信号以便讓 程序繼續執行。那麼我們如何防止這種發生呢?

  答案是安裝一個處理器處理進來的信号并在發生時捕獲它們

第一步:建立信号處理器

  信号是核心傳給某個程序的一個整數。當程序接收到信号,它便以以下方式之一響應:

  • 忽略該信号;
  • 讓核心完成與該信号關聯的預設操作;
  • 捕獲該信号,即讓核心将控制傳給信号處理例程,等信号處理例程執行完畢,然後又從中斷的地方恢複程式的執行。

  所謂信号處理例程是一個函數,當某個信号發生時,核心會自動調用該函數。signal()函數為給定的信号注冊一個處理例程:

typedef void (*handler)(void);
void * signal(int signum, handler);      

  第一個參數是信号編碼。第二個參數使用者定義的函數位址,當信号 signum 産生時,handler 所指向的函數被調用。

除了函數位址之外,第二個參數也可以是兩個特殊的值:SIG_IGN 和 SIG_DFL。SIG_IGN 表示該信号應被忽略(注意:SIGKILL 和 SIGSTOP 在無論如何都是不能被阻塞、捕獲或忽略的);SIG_DFL 訓示核心該信号産生時完成預設行為。

第二步:發信号

向某個程序發信号有三種方式:

  • 程序通過條用 raise() 顯式地發送信号給自己;
  • 信号從另一個程序發送,比方說通過 kill() 系統調用或者 Perl 腳本;
  • 信号從核心發送。例如,當程序試圖存取不屬于自己的記憶體,或在系統關閉期間存取記憶體時;

第三步:産生和處理信号

下面程式注冊 SIGTERM 處理器。然後産生一個 SIGTERM 信号,進而導緻該處理器運作:

#include <csignal>
#include <iostream>
using namespace std;

void term(int sig)
{
	//..necessary cleanup operations before terminating
	cout << "handling signal no." <<sig <<endl;
}

int main()
{
	signal(SIGTERM, term); // register a SIGTERM handler
	raise(SIGTERM); // will cause term() to run
}              

ANSI <signal.h> 的局限

  當進入就緒狀态的某個程序準備運作一個 SIGx 信号處理例程時又接收到另一個 SIGx 信号,這時會發生什麼情況呢?一個方法是讓核心中斷該程序并再次運作該信号處理例程。為此,這個處理例程必須是可重入的( re-entrant)。但是,設計可重入的處理例程決非易事。ANSI C 解決 重制信号(recurring signals)問題的方法是在執行使用者定義的處理例程前,将處理例程重置為 STG_DFL。這樣做是有問題的。

  當兩個信号快速産生時,核心運作第一個信号的處理例程,而對第二個信号則進行預設處理,這樣有可能終止該程序。

  在過去的三十年中出現了幾個可以信号處理架構,每一種架構對重制信号的處理問題提供了不同的解決方法。 POSIX 信号 API 是其中最為成熟的和可移植的一個。

POSIX 信号

  POSIX 信号處理函數操作一組打包在 sigset_t 資料類型中信号:

  • int sigemptyset(sigset_t * pset); 清除 pset 中的所有信号。
  • int sigfillset(sigset_t * pset); 用可獲得的信号填充 pset。
  • int sigaddset(sigset_t * pset, int signum); 将 signum 添加到 pset。
  • int sigdelset(sigset_t * pset, int signum); 從 pset 中删除 signum。
  • int sigismember(const sigset_t * pset, int signum); 如果 signum 包含在 pset 中,則傳回非零,否則傳回 0。

Sigaction() 為特定的信号注冊處理例程:

int sigaction(int signum, struct sigaction * act, struct sigaction *prev);       

sigaction 結構描述核心處理 signum 的資訊:

struct sigaction
{
	sighanlder_t sa_hanlder; 
	sigset_t sa_mask;          // 阻塞信号的清單
	unsigned long sa_flags;    // 阻塞模式
	void (*sa_restorer)(void); // 未使用
};
      

  sa_hanlder 儲存函數的位址,該函數帶一個整型參數,沒有傳回值。它還可以是兩個特别值之一:SIG_DFL 和 SIG_IGN。

額外特性

  POSIX API 提供多種 ANSI 庫中所沒有的服務。其中包括阻塞進入的信号并擷取目前未決信号。

阻塞信号

  sigprocmask() 阻塞和取消阻塞信号:

int sigprocmask(int mode, const sigset_t* newmask,sigset_t * oldmask);       

mode 可取以下值之一:

SIG_BLOCK —— 将 newmask 中的信号添加到目前的信号擋闆中。
SIG_UNBLOCK —— 從目前的信号擋闆中删除 newmask 信号。
SIG_SETMASK —— 僅阻塞 newmask 中的信号。      

擷取未決信号

  阻塞的信号處于等待狀态,直到程序就緒接收它們。這樣的信号被稱為未決信号,可以通過調用 sigpending() 來擷取。

int sigpending(sigset_t * pset);       

繼續閱讀