首頁  >  文章  >  系統教程  >  Linux 下的訊號機制:如何使用訊號進行進程間通訊與控制

Linux 下的訊號機制:如何使用訊號進行進程間通訊與控制

WBOY
WBOY轉載
2024-02-12 12:40:031124瀏覽

Linux 下的信号机制:如何使用信号进行进程间通信和控制

訊號是Linux 系統中一種常用的進程間通訊和控制的方法,它可以讓一個進程向另一個進程發送一個簡單的訊息,通知它發生了某種事件或狀態。訊號的作用是提高系統的反應性和靈活性,以應對一些異常或緊急的情況。在 Linux 系統中,有許多種訊號,如 SIGINT、SIGTERM、SIGKILL 等,它們各有各的意義和作用,適用於不同的場景和需求。但是,你真的了解 Linux 下的訊號機制嗎?你知道如何在 Linux 下使用訊號進行進程間通訊和控制嗎?你知道如何在 Linux 下處理和忽略訊號嗎?本文將為你詳細介紹 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

上述聲明格式比較複雜,如果不清楚如何使用,也可以透過下面這種類型定義的格式來使用(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 
\#include 
\#include 
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 
\#include 

该 系统调用可以用来向任何进程或进程组发送任何信号。如果参数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

该调用使得发出调用的进程进入睡眠,直到接收到一个信号为止。该调用总是返回-1,并设置错误代码为EINTR(接收到一个信号)。下面是一个简单的范例:

#include 
\#include 
\#include 
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

系 统调用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

该系统调用给进程提供了三个定时器,它们各自有其独有的计时域,当其中任何一个到达,就发送一个相应的信号给进程,并使得计时器重新开始。三个计时器由参数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 
\#include 
\#include 
\#include 
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 下的信号机制有了一个深入的了解,知道了它的定义、原理、用法和优缺点。你也应该明白了信号机制的作用和影响,以及如何在 Linux 下正确地使用和处理信号。我们建议你在使用 Linux 系统时,使用信号机制来提高系统的响应性和灵活性。同时,我们也提醒你在使用信号机制时要注意一些潜在的问题和挑战,如信号丢失、信号屏蔽、信号安全等。希望本文能够帮助你更好地使用 Linux 系统,让你在 Linux 下掌握信号机制的使用和处理。

以上是Linux 下的訊號機制:如何使用訊號進行進程間通訊與控制的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:lxlinux.net。如有侵權,請聯絡admin@php.cn刪除