Linux訊號處理機制

來源:互聯網
上載者:User
訊號是Linux編程中非常重要的部分,本文將詳細介紹訊號機制的基本概念、Linux對訊號機制的大致實現方法、如何使用訊號,以及有關訊號的幾個系統調用。

訊號機制是進程之間相互傳遞訊息的一種方法,訊號全稱為非強制中斷訊號,也有人稱作非強制中斷。從它的命名可以看出,它的實質和使用很象中斷。所以,訊號可以說是進程式控制制的一部分。

一、訊號的基本概念

本節先介紹訊號的一些基本概念,然後給出一些基本的訊號類型和訊號對應的事件。基本概念對於理解和使用訊號,對於理解訊號機制都特別重要。下面就來看看什麼是訊號。

1、基本概念

非強制中斷訊號(signal,又簡稱為訊號)用來通知進程發生了非同步事件。進程之間可以互相通過系統調用kill發送非強制中斷訊號。核心也可以因為內部事件而給進程發送訊號,通知進程發生了某個事件。注意,訊號只是用來通知某進程發生了什麼事件,並不給該進程傳遞任何資料。

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

在進程表的表項中有一個非強制中斷訊號域,該域中每一位對應一個訊號,當有訊號發送給進程時,對應位置位。由此可以看出,進程對不同的訊號可以同時保留,但對於同一個訊號,進程並不知道在處理之前來過多少個。

2、訊號的類型

發出訊號的原因很多,這裡按發出訊號的原因簡單分類,以瞭解各種訊號:

(1) 與進程終止相關的訊號。當進程退出,或者子進程終止時,發出這類訊號。
(2) 與進程例外事件相關的訊號。如進程越界,或企圖寫一個唯讀記憶體地區(如程式本文區),或執行一個特權指令及其他各種硬體錯誤。
(3) 與在系統調用期間遇到不可恢複條件相關的訊號。如執行系統調用exec時,原有資源已經釋放,而目前系統資源又已經耗盡。
(4) 與執行系統調用時遇到非預測錯誤條件相關的訊號。如執行一個並不存在的系統調用。
(5) 在使用者態下的進程發出的訊號。如進程調用系統調用kill向其他進程發送訊號。
(6) 與終端互動相關的訊號。如使用者關閉一個終端,或按下break鍵等情況。
(7) 跟蹤進程執行的訊號。

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) { /* 訊號處理常式,其中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); //* 下面設定三個訊號的處理方法
signal(SIGINT, sigroutine);
signal(SIGQUIT, sigroutine);
for (;;) ;
}

其中訊號SIGINT由按下Ctrl-C發出,訊號SIGQUIT由按下Ctrl-發出。該程式執行的結果如下:

localhost:~$ ./sig_test
process id is 463
Get a signal -SIGINT //按下Ctrl-C得到的結果
Get a signal -SIGQUIT //按下Ctrl-得到的結果
//按下Ctrl-z將進程置於後台
[1]+ Stopped ./sig_test
localhost:~$ bg
[1]+ ./sig_test &
localhost:~$ kill -HUP 463 //向進程發送SIGHUP訊號
localhost:~$ Get a signal – SIGHUP
kill -9 463 //向進程發送SIGKILL訊號,終止進程
localhost:~$

2、kill 系統調用

系統調用kill用來向進程發送一個訊號。該調用聲明的格式如下:
int kill(pid_t pid, int sig);
在使用該調用的進程中加入以下標頭檔:
#include <sys/types.h>
#include <signal.h>

該 系統調用可以用來向任何進程或進程組發送任何訊號。如果參數pid是正數,那麼該調用將訊號sig發送到進程號為pid的進程。如果pid等於0,那麼信 號sig將發送給當前進程所屬進程組裡的所有進程。如果參數pid等於-1,訊號sig將發送給除了進程1和自身以外的所有進程。如果參數pid小於- 1,訊號sig將發送給屬於進程組-pid的所有進程。如果參數sig為0,將不發送訊號。該調用執行成功時,傳回值為0;錯誤時,返回-1,並設定相應 的錯誤碼errno。下面是一些可能返回的錯誤碼:

EINVAL:指定的訊號sig無效。
ESRCH:參數pid指定的進程或進程組不存在。注意,在進程表項中存在的進程,可能是一個還沒有被wait收回,但已經終止執行的僵死進程。
EPERM: 進程沒有權力將這個訊號發送到指定接收訊號的進程。因為,一個進程被允許將訊號發送到進程pid時,必須擁有root權力,或者是發出調用的進程的UID 或EUID與指定接收的進程的UID或儲存使用者ID(savedset-user-ID)相同。如果參數pid小於-1,即該訊號發送給一個組,則該錯誤 表示組中有成員進程不能接收該訊號。

3、pause系統調用

系統調用pause的作用是等待一個訊號。該調用的聲明格式如下:
int pause(void);
在使用該調用的進程中加入以下標頭檔:
#include <unistd.h>

該調用使得發出調用的進程進入睡眠,直到接收到一個訊號為止。該調用總是返回-1,並設定錯誤碼為EINTR(接收到一個訊號)。下面是一個簡單的範例:

#include <unistd.h>
#include <stdio.h>
#include <signal.h>
void sigroutine(int unused) {
printf("Catch a signal SIGINT ");
}

int main() {
signal(SIGINT, sigroutine);
pause();
printf("receive a signal ");
}

在這個例子中,程式開始執行,就象進入了死迴圈一樣,這是因為進程正在等待訊號,當我們按下Ctrl-C時,訊號被捕捉,並且使得pause退出等待狀態。

4、alarm和 setitimer系統調用

系統調用alarm的功能是設定一個定時器,當定時器計時到達時,將發出一個訊號給進程。該調用的聲明格式如下:
unsigned int alarm(unsigned int seconds);
在使用該調用的進程中加入以下標頭檔:
#include <unistd.h>

系 統調用alarm安排核心為調用進程在指定的seconds秒後發出一個SIGALRM的訊號。如果指定的參數seconds為0,則不再發送 SIGALRM訊號。後一次設定將取消前一次的設定。該調用傳回值為上次定時調用到發送之間剩餘的時間,或者因為沒有前一次定時調用而返回0。

注意,在使用時,alarm只設定為發送一次訊號,如果要多次發送,就要多次使用alarm調用。

對於alarm,這裡不再舉例。現在的系統中很多程式不再使用alarm調用,而是使用setitimer調用來設定定時器,用getitimer來得到定時器的狀態,這兩個調用的聲明格式如下:

int getitimer(int which, struct itimerval *value);
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);

在使用這兩個調用的進程中加入以下標頭檔:
#include <sys/time.h>

該系統調用給進程提供了三個定時器,它們各自有其專屬的計時域,當其中任何一個到達,就發送一個相應的訊號給進程,並使得計時器重新開始。三個計時器由參數which指定,如下所示:

TIMER_REAL:按實際時間計時,計時到達將給進程發送SIGALRM訊號。
ITIMER_VIRTUAL:僅當進程執行時才進行計時。計時到達將發送SIGVTALRM訊號給進程。
ITIMER_PROF:當進程執行時和系統為該進程執行動作時都計時。與ITIMER_VIR-TUAL是一對,該定時器經常用來統計進程在使用者態和核心態花費的時間。計時到達將發送SIGPROF訊號給進程。

定時器中的參數value用來指明定時器的時間,其結構如下:
struct itimerval {
struct timeval it_interval; /* 下一次的取值 */
struct timeval it_value; /* 本次的設定值 */
};

該結構中timeval結構定義如下:
struct timeval {
long tv_sec; /* 秒 */
long tv_usec; /* 微秒,1秒 = 1000000 微秒*/
};

在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

本文簡單介紹了Linux下的訊號,如果希望瞭解其他調用,請參考線上手冊或其他文檔。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.