Linux系統程式設計——程序間通訊:訊號中斷處理
什麼是訊號?
訊號是 Linux 程序間通訊的最古老的方式。訊號是軟體中斷,它是在軟體層次上對中斷機制的一種模擬,是一種非同步通訊的方式 。訊號可以導致一個正在執行的程序被另一個正在執行的非同步程序中斷,轉而處理某一個突發事件。
“中斷”在我們生活中經常遇到,譬如,我正在房間裡打遊戲,突然送快遞的來了,把正在玩遊戲的我給“中斷”了,我去簽收快遞( 處理中斷 ),處理完成後,再繼續玩我的遊戲。這裡我們學習的“訊號”就是屬於這麼一種“中斷”。我們在終端上敲“Ctrl+c”,就產生一個“中斷”,相當於產生一個訊號,接著就會處理這麼一個“中斷任務”(預設的處理方式為中斷當前程序)。
訊號可以直接進行使用者空間程序和核心空間程序的互動,核心程序可以利用它來通知使用者空間程序發生了哪些系統事件。
一個完整的訊號週期包括三個部分:訊號的產生,訊號在程序中的註冊,訊號在程序中的登出,執行訊號處理函式。如下圖所示:
注意:這裡訊號的產生,註冊,登出時訊號的內部機制,而不是訊號的函式實現。
Linux 可使用命令:kill -l("l" 為字母),檢視相應的訊號。
列表中,編號為 1 ~ 31 的訊號為傳統 UNIX 支援的訊號,是不可靠訊號(非實時的),編號為 32 ~ 63 的訊號是後來擴充的,稱做可靠訊號(實時訊號)
。不可靠訊號和可靠訊號的區別在於前者不支援排隊,可能會造成訊號丟失,而後者不會。非可靠訊號一般都有確定的用途及含義, 可靠訊號則可以讓使用者自定義使用。更多詳情,請看《Linux訊號列表》。
訊號的產生方式
1)當用戶按某些終端鍵時,將產生訊號。
終端上按“Ctrl+c”組合鍵通常產生中斷訊號 SIGINT,終端上按“Ctrl+\”鍵通常產生中斷訊號 SIGQUIT,終端上按“Ctrl+z”鍵通常產生中斷訊號 SIGSTOP 等。
2)硬體異常將產生訊號。
除數為 0,無效的記憶體訪問等。這些情況通常由硬體檢測到,並通知核心,然後核心產生適當的訊號傳送給相應的程序。
3)軟體異常將產生訊號。
當檢測到某種軟體條件已發生,並將其通知有關程序時,產生訊號。
4)呼叫 kill() 函式將傳送訊號。
注意:接收訊號程序和傳送訊號程序的所有者必須相同,或傳送訊號程序的所有者必須是超級使用者。
5)執行 kill 命令將傳送訊號。
此程式實際上是使用 kill 函式來發送訊號。也常用此命令終止一個失控的後臺程序。
訊號的常用操作
傳送訊號
所需標頭檔案:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int signum);
功能:給指定程序傳送訊號。
注意:使用 kill() 函式傳送訊號,接收訊號程序和傳送訊號程序的所有者必須相同,或者傳送訊號程序的所有者是超級使用者。
引數: pid: 取值有 4 種情況:
pid > 0: 將訊號傳送給程序 ID 為pid的程序。
pid = 0: 將訊號傳送給當前程序所在程序組中的所有程序。
pid = -1: 將訊號傳送給系統內所有的程序。
pid < -1: 將訊號傳給指定程序組的所有程序。這個程序組號等於 pid 的絕對值。
signum: 訊號的編號,這裡可以填數字編號,也可以填訊號的巨集定義,可以通過命令 kill -l ("l" 為字母)進行相應檢視。
返回值:
成功:0
失敗:-1
下面為測試程式碼,本來父子程序各自每隔一秒列印一句話,3 秒後,父程序通過 kill() 函式給子程序傳送一箇中斷訊號 SIGINT( 2 號訊號),最終,子程序結束,剩下父程序在列印資訊:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
int main(int argc, char *argv[])
{
pid_t pid;
int i = 0;
pid = fork(); // 建立程序
if( pid < 0 ){ // 出錯
perror("fork");
}
if(pid == 0){ // 子程序
while(1){
printf("I am son\n");
sleep(1);
}
}else if(pid > 0){ // 父程序
while(1){
printf("I am father\n");
sleep(1);
i++;
if(3 == i){// 3秒後
kill(pid, SIGINT); // 給子程序 pid ,傳送中斷訊號 SIGINT
// kill(pid, 2); // 等級於kill(pid, SIGINT);
}
}
}
return 0;
}
執行結果如下:
等待訊號
所需標頭檔案:
#include <unistd.h>
int pause(void);
功能:
等待訊號的到來(此函式會阻塞)。將呼叫程序掛起直至捕捉到訊號為止,此函式通常用於判斷訊號是否已到。
引數: 無
返回值:
直到捕獲到訊號才返回 -1,且 errno 被設定成 EINTR。
測試程式碼如下:
#include <unistd.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("in pause function\n");
pause();
return 0;
}
沒有產生訊號前,程序一直阻塞在 pause() 不會往下執行,假如,我們按“Ctrl+c”,pause() 會捕獲到此訊號,中斷當前程序。
處理訊號
一個程序收到一個訊號的時候,可以用如下方法進行處理:
1)執行系統預設動作
對大多數訊號來說,系統預設動作是用來終止該程序。
2)忽略此訊號
接收到此訊號後沒有任何動作。
3)執行自定義訊號處理函式
用使用者定義的訊號處理函式處理該訊號。
注意:SIGKILL 和 SIGSTOP 不能更改訊號的處理方式,因為它們向用戶提供了一種使程序終止的可靠方法。
產生一個訊號,我們可以讓其執行自定義訊號處理函式。假如有函式 A, B, C,我們如何確定訊號產生後只調用函式 A,而不是函式 B 或 C。這時候,我們需要一種規則規定,訊號產生後就呼叫函式 A,就像交通規則一樣,紅燈走綠燈行,訊號註冊函式 signal() 就是做這樣的事情。
所需標頭檔案:
#include <signal.h>
typedef void (*sighandler_t)(int);// 回撥函式的宣告
sighandler_t signal(int signum,sighandler_t handler);功能:
註冊訊號處理函式(不可用於 SIGKILL、SIGSTOP 訊號),即確定收到訊號後處理函式的入口地址。此函式不會阻塞。
引數:
signum:訊號的編號,這裡可以填數字編號,也可以填訊號的巨集定義,可以通過命令 kill -l ("l" 為字母)進行相應檢視。
handler: 取值有 3 種情況:
- SIG_IGN:忽略該訊號
- SIG_DFL:執行系統預設動作
- 訊號處理函式名:自定義訊號處理函式 ,如:fun
回撥函式的定義如下:
void fun(int signo)
{
// signo 為觸發的訊號,為 signal() 第一個引數的值
}
注意:訊號處理函式應該為可重入函式,關於可重入函式的更多詳情,請《淺談可重入函式與不可重入函式》。
返回值:
成功:第一次返回 NULL,下一次返回此訊號上一次註冊的訊號處理函式的地址。如果需要使用此返回值,必須在前面先宣告此函式指標的型別。
失敗:返回 SIG_ERR
示例一:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
// 訊號處理函式
void signal_handler(int signo)
{
if(signo == SIGINT){
printf("recv SIGINT\n");
}else if(signo == SIGQUIT){
printf("recv SIGQUIT\n");
}
}
int main(int argc, char *argv[])
{
printf("wait for SIGINT OR SIGQUIT\n");
/* SIGINT: Ctrl+c ; SIGQUIT: Ctrl+\ */
// 訊號註冊函式
signal(SIGINT, signal_handler);
signal(SIGQUIT, signal_handler);
// 等待訊號
pause();
pause();
return 0;
}
在終端裡敲“Ctrl+c”或“Ctrl+\”,自動呼叫其指定好的回撥函式 signal_handler():
示例二:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
// 回撥函式的宣告
typedef void (*sighandler_t)(int);
void fun1(int signo)
{
printf("in fun1\n");
}
void fun2(int signo)
{
printf("in fun2\n");
}
int main(int argc, char *argv[])
{
sighandler_t previous = NULL;
// 第一次返回 NULL
previous = signal(SIGINT,fun1);
if(previous == NULL)
{
printf("return fun addr is NULL\n");
}
// 下一次返回此訊號上一次註冊的訊號處理函式的地址。
previous = signal(SIGINT, fun2);
if(previous == fun1)
{
printf("return fun addr is fun1\n");
}
// 還是返回 NULL,因為處理的訊號變了
previous = signal(SIGQUIT,fun1);
if(previous == NULL)
{
printf("return fun addr is NULL\n");
}
return 0;
}
執行結果如下:
訊號集與訊號阻塞集
訊號集
為了方便對多個訊號進行處理,一個使用者程序常常需要對多個訊號做出處理,在 Linux 系統中引入了訊號集(訊號的集合)。這個訊號集有點類似於我們的 QQ 群,一個個的訊號相當於 QQ 群裡的一個個好友。
訊號集是用來表示多個訊號的資料型別(sigset_t),其定義路徑為:/usr/include/i386-linux-gnu/bits/sigset.h。
訊號集相關的操作主要有如下幾個函式:
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigismember(const sigset_t *set, int signum);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
以上幾個函式的用法都是比較簡單,這裡就不一一介紹,我們通過一個例子來學習其用法:
#include <signal.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
sigset_t set; // 定義一個訊號集變數
int ret = 0;
sigemptyset(&set); // 清空訊號集的內容
// 判斷 SIGINT 是否在訊號集 set 裡
// 在返回 1, 不在返回 0
ret = sigismember(&set, SIGINT);
if(ret == 0){
printf("SIGINT is not a member of set \nret = %d\n", ret);
}
sigaddset(&set, SIGINT); // 把 SIGINT 新增到訊號集 set
sigaddset(&set, SIGQUIT);// 把 SIGQUIT 新增到訊號集 set
// 判斷 SIGINT 是否在訊號集 set 裡
// 在返回 1, 不在返回 0
ret = sigismember(&set, SIGINT);
if(ret == 1){
printf("SIGINT is a member of set \nret = %d\n", ret);
}
sigdelset(&set, SIGQUIT); // 把 SIGQUIT 從訊號集 set 移除
// 判斷 SIGQUIT 是否在訊號集 set 裡
// 在返回 1, 不在返回 0
ret = sigismember(&set, SIGQUIT);
if(ret == 0){
printf("SIGQUIT is not a member of set \nret = %d\n", ret);
}
return 0;
}
執行結果如下:
訊號阻塞集(遮蔽集、掩碼)
訊號阻塞集也稱訊號遮蔽集、訊號掩碼。每個程序都有一個阻塞集,建立子程序時子程序將繼承父程序的阻塞集。訊號阻塞集用來描述哪些訊號遞送到該程序的時候被阻塞(在訊號發生時記住它,直到程序準備好時再將訊號通知程序)。
所謂阻塞並不是禁止傳送訊號, 而是暫緩訊號的傳送。若將被阻塞的訊號從訊號阻塞集中刪除,且對應的訊號在被阻塞時發生了,程序將會收到相應的訊號。
我們可以通過 sigprocmask() 修改當前的訊號掩碼來改變訊號的阻塞情況。
所需標頭檔案:
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
功能:
檢查或修改訊號阻塞集,根據 how 指定的方法對程序的阻塞集合進行修改,新的訊號阻塞集由 set 指定,而原先的訊號阻塞集合由 oldset 儲存。
引數:
how: 訊號阻塞集合的修改方法,有 3 種情況:
SIG_BLOCK:向訊號阻塞集合中新增 set 訊號集,新的訊號掩碼是set和舊訊號掩碼的並集。
SIG_UNBLOCK:從訊號阻塞集合中刪除 set 訊號集,從當前訊號掩碼中去除 set 中的訊號。
SIG_SETMASK:將訊號阻塞集合設為 set 訊號集,相當於原來訊號阻塞集的內容清空,然後按照 set 中的訊號重新設定訊號阻塞集。
set: 要操作的訊號集地址。
若 set 為 NULL,則不改變訊號阻塞集合,函式只把當前訊號阻塞集合儲存到 oldset 中。
oldset: 儲存原先訊號阻塞集地址
返回值:
成功:0,
失敗:-1,失敗時錯誤程式碼只可能是 EINVAL,表示引數 how 不合法。
注意:不能阻塞 SIGKILL 和 SIGSTOP 等訊號,但是當 set 引數包含這些訊號時 sigprocmask() 不返回錯誤,只是忽略它們。另外,阻塞 SIGFPE 這樣的訊號可能導致不可挽回的結果,因為這些訊號是由程式錯誤產生的,忽略它們只能導致程式無法執行而被終止。
示例程式碼如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
int main(int argc, char *argv[])
{
sigset_t set; // 訊號集合
int i = 0;
sigemptyset(&set); // 清空訊號集合
sigaddset(&set, SIGINT); // SIGINT 加入 set 集合
while(1)
{
// set 集合加入阻塞集,在沒有移除前,SIGINT 會被阻塞
sigprocmask(SIG_BLOCK, &set, NULL);
for(i=0; i<5; i++)
{
printf("SIGINT signal is blocked\n");
sleep(1);
}
// set 集合從阻塞集中移除
// 假如 SIGINT 訊號在被阻塞時發生了
// 此刻,SIGINT 訊號立馬生效,中斷當前程序
sigprocmask(SIG_UNBLOCK, &set, NULL);
for(i=0; i<5; i++)
{
printf("SIGINT signal unblocked\n");
sleep(1);
}
}
return 0;
}
可靠訊號的操作
從 UNIX 系統繼承過來的訊號(SIGHUP~SIGSYS,前 32 個)都是不可靠訊號,不支援排隊(多次傳送相同的訊號, 程序可能只能收到一次,可能會丟失)。
SIGRTMIN 至 SIGRTMAX 的訊號支援排隊(發多少次, 就可以收到多少次, 不會丟失),故稱為可靠訊號。
可靠訊號就是實時訊號,非可靠訊號就是非實時訊號。
signal() 函式只能提供簡單的訊號安裝操作,使用 signal() 函式處理訊號比較簡單,只要把要處理的訊號和處理函式列出即可。signal() 函式主要用於前面 32 種不可靠、非實時訊號的處理,並且不支援訊號傳遞資訊。
Linux 提供了功能更強大的 sigaction() 函式,此函式可以用來檢查和更改訊號處理操作,可以支援可靠、實時訊號的處理,並且支援訊號傳遞資訊。
下面我們一起學習其相關函式的使用。
所需標頭檔案:
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
功能:給指定程序傳送訊號
引數:
pid: 程序號。
sig: 訊號的編號,這裡可以填數字編號,也可以填訊號的巨集定義,可以通過命令 kill -l ("l" 為字母)進行相應檢視。
value: 通過訊號傳遞的引數。
union sigval 型別如下:
union sigval { int sival_int; void *sival_ptr; };
返回值:
成功:0
失敗:-1
int sigaction(int signum,const struct sigaction *act, struct sigaction *oldact );
功能:檢查或修改指定訊號的設定(或同時執行這兩種操作)。
引數:
signum:要操作的訊號。
act: 要設定的對訊號的新處理方式(設定)。
oldact:原來對訊號的處理方式(設定)。
如果 act 指標非空,則要改變指定訊號的處理方式(設定),如果 oldact 指標非空,則系統將此前指定訊號的處理方式 (設定)存入 oldact。
返回值:
成功:0
失敗:-1
訊號設定結構體:
struct sigaction
{
/*舊的訊號處理函式指標*/
void (*sa_handler)(int signum) ;
/*新的訊號處理函式指標*/
void (*sa_sigaction)(int signum, siginfo_t *info, void *context);
sigset_t sa_mask;/*訊號阻塞集*/
int sa_flags;/*訊號處理的方式*/
};
sa_handler、sa_sigaction:訊號處理函式指標,和 signal() 裡的函式指標用法一樣,應根據情況給 sa_sigaction、sa_handler 兩者之一賦值,其取值如下:
- SIG_IGN:忽略該訊號
- SIG_DFL:執行系統預設動作
- 處理函式名:自定義訊號處理函式
sa_flags:用於指定訊號處理的行為,它可以是一下值的“按位或”組合:
- SA_RESTART:使被訊號打斷的系統呼叫自動重新發起(已經廢棄)
- SA_NOCLDSTOP:使父程序在它的子程序暫停或繼續執行時不會收到 SIGCHLD 訊號。
- SA_NOCLDWAIT:使父程序在它的子程序退出時不會收到 SIGCHLD 訊號,這時子程序如果退出也不會成為殭屍程序。
- SA_NODEFER:使對訊號的遮蔽無效,即在訊號處理函式執行期間仍能發出這個訊號。
- SA_RESETHAND:訊號處理之後重新設定為預設的處理方式。
- SA_SIGINFO:使用 sa_sigaction 成員而不是 sa_handler 作為訊號處理函式。
訊號處理函式:
void (*sa_sigaction)( int signum, siginfo_t *info, void *context );
引數說明:signum:訊號的編號。
info:記錄訊號傳送程序資訊的結構體,程序資訊結構體路徑:/usr/include/i386-linux-gnu/bits/siginfo.h
context:可以賦給指向 ucontext_t 型別的一個物件的指標,以引用在傳遞訊號時被中斷的接收程序或執行緒的上下文
下面我們做這麼一個例子,一個程序在傳送訊號,一個程序在接收訊號的傳送。
傳送訊號示例程式碼如下:
#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
/*******************************************************
*功能: 發 SIGINT 訊號及訊號攜帶的值給指定的程序
*引數: argv[1]:程序號
argv[2]:待發送的值(預設為100)
*返回值: 0
********************************************************/
int main(int argc, char *argv[])
{
if(argc >= 2)
{
pid_t pid,pid_self;
union sigval tmp;
pid = atoi(argv[1]); // 程序號
if( argc >= 3 )
{
tmp.sival_int = atoi(argv[2]);
}
else
{
tmp.sival_int = 100;
}
// 給程序 pid,傳送 SIGINT 訊號,並把 tmp 傳遞過去
sigqueue(pid, SIGINT, tmp);
pid_self = getpid(); // 程序號
printf("pid = %d, pid_self = %d\n", pid, pid_self);
}
return 0;
}
接收訊號示例程式碼如下:
#include <signal.h>
#include <stdio.h>
// 訊號處理回電函式
void signal_handler(int signum, siginfo_t *info, void *ptr)
{
printf("signum = %d\n", signum); // 訊號編號
printf("info->si_pid = %d\n", info->si_pid); // 對方的程序號
printf("info->si_sigval = %d\n", info->si_value.sival_int); // 對方傳遞過來的資訊
}
int main(int argc, char *argv[])
{
struct sigaction act, oact;
act.sa_sigaction = signal_handler; //指定訊號處理回撥函式
sigemptyset(&act.sa_mask); // 阻塞集為空
act.sa_flags = SA_SIGINFO; // 指定呼叫 signal_handler
// 註冊訊號 SIGINT
sigaction(SIGINT, &act, &oact);
while(1)
{
printf("pid is %d\n", getpid()); // 程序號
pause(); // 捕獲訊號,此函式會阻塞
}
return 0;
}
兩個終端分別編譯程式碼,一個程序接收,一個程序傳送,執行結果如下: