第十章 信號
信號是軟件中斷,它提供了一種處理異步事件的方法。本章對信號機制進行綜述,並說明每種信號的一般用法。
1、信號概念
每個信號都有一個名字,這些名字都以3個字符SIG開頭。在頭文件<signal.h>中,信號名都被定義爲正整形常量。
1.1 產生信號的條件:
(1)當用戶按某寫終端按鍵時,引發終端產生的信號。如:Ctrl+C產生SIGINT信號。
(2)硬件異常產生信號。如除零錯,無效的內存應用產生SIGSEGV信號。
(3)進程調用kill函數可將任一信號發送給另一個進程或進程組。
(4)用戶可用kill命令將信號發送給其他進程。
(5)當檢測到某種軟件條件已經發生,並應將其通知有關進程時也產生信號。如SIGPIPE在管道的讀進程已終止後,一個進程寫此管道。
1.2 在某個信號出現時,可以按下列3種方式之一進行處理:
(1) 忽略該信號。大多數信號都可以使用這種方式進行處理,但有兩種信號卻決不能被忽略:SIGKILL和SIGSTOP(只能執行系統默認動作)。
(2) 捕獲信號。通知內核在某信號發生時,調用一個用戶函數對這種時間進行處理。
(3) 執行系統默認動作。對於大多數信號的系統默認動作是終止該進程。
2、信號類型
程序錯誤類信號:默認動作使進程流產,產生core文件。
SIGABRT: 調用abort函數生成的信號。
SIGFPE: 浮點計算錯誤。
SIGILL: 非法指令錯誤。
SIGBUS/SIGSEGV: 硬件錯誤-非法地址訪問。
SIGEMT: 硬件錯誤
SIGSYS: 非法系統調用。
SIGTRAP: 硬件錯誤(通常爲斷點指令)。
程序終止類信號:默認動作使進程終止,我們通常要處理這類信號,做一些清理工作,句柄函數應在結束時爲此信號指定默認動作,然後再次生成該信號,使得程序終止。
SIGHUP:終端斷開連接時,生成此信號給控制進程。
SIGINT:Ctrl-C或Delete按下時,由終端驅動生成,併發送給前臺進程組中的所有進程。
SIGKILL:使程序立即終止,不能被捕獲或忽略,也不能被阻塞。
SIGQUIT:Ctrl-\,如SIGINT,並且產生core。
SIGTERM:該信號使程序終止,但是可以阻塞、捕獲、忽略。
鬧鐘類信號:通知定時器到期,默認動作是終止程序,但通常會設置句柄。
SIGALRM:alarm/setitimer函數設置定時到期後,會產生此信號。
SIGPROF:
SIGVTALRM:
I/O類信號:通知進程在描述字上發生了感興趣事件,支持信號驅動IO。
SIGIO: fd準備執行輸入輸出時發送此信號。
SIGPOLL:異步I/O信號。
SIGURG:網絡收到帶外數據時可選擇生成此信號。
作業控制類信號:
SIGCHLD: 進程終止或停止時會向其父進程發送該信號,默認動作爲忽略。
SIGCONT: 使停止的進程恢復運行。
SIGSTOP: 停止進程。
SIGTSTP/SIGTTIN/SIGTTOU:
操作錯誤類信號:默認動作終止程序。
SIGPIPE: 管道破裂。
SIGXCPU/SIGXFSZ:
3、函數signal
UNIX系統信號機制最簡單的接口是signal函數,不過最好使用sigaction代替該函數。
#include <signal.h>
void (*signal(int signo,void (*func)(int)))(int);
//返回值:若成功,返回以前的信號處理配置;若出錯,返回SIG_ERR。
//可以使用typedef使其變得簡單一點
typedef void Sigfunc(int);
Sigfunc *signal(int,Sigfunc *);
第一個參數指定要捕獲的信號(整形常量),第二個參數指定針對前面信號值的處理handler可以是SIG_IGN(忽略該信號)、SIG_DFL(系統默認動作)或者是接到此信號後要調用的函數地址。我們稱這種處理爲捕捉該信號,稱此函數爲信號處理程序或信號捕捉函數,第二個參數是一個函數指針(處理函數),該函數指針指向的函數返回值是void,參數是int。
下面給出一個簡單得信號處理程序:
#include "apue.h"
static void sig_usr(int); /* one handler for both signals */
int main(void)
{
if (signal(SIGUSR1, sig_usr) == SIG_ERR)
err_sys("can't catch SIGUSR1");
if (signal(SIGUSR2, sig_usr) == SIG_ERR)
err_sys("can't catch SIGUSR2");
for ( ; ; )
pause();
}
static void sig_usr(int signo) /* argument is signal number */
{
if (signo == SIGUSR1)
printf("received SIGUSR1\n");
else if (signo == SIGUSR2)
printf("received SIGUSR2\n");
else
err_dump("received signal %d\n", signo);
}
我們使該程序在後臺運行,而且用kill命令將信號發送給它:
4、中斷的系統調用
早期UNIX系統的一個特性是:如果進程在執行一個低速系統調用而阻塞期間捕獲到一個信號,則該系統調用就被中斷不再繼續執行。該系統調用返回出錯,其errno設置爲EINTR。後面的章節會更多的涉及到被中斷的系統調用。
5、可重入函數
進程捕捉的信號並對其進行處理時,進程正在執行的正常指令序列就被信號處理程序臨時終端,它首先執行該信號處理程序中的指令,如果從信號處理程序返回,則繼續執行在捕捉的信號時進程正在執行的正常指令序列。但是在返回控制時不會出現任何錯誤,就叫可重入函數。而不可重入的函數由於使用了一些系統資源,比如全局變量區,中斷向量表等,所以它如果被中斷的話,可能會出現問題,這類函數是不能運行在多任務環境下的。
在信號處理函數中調用某些函數可能對導致安全問題(其結果是不可預知的),下面列出了這些異步信號安全的函數,沒有列入圖中的大多數函數是不可重入的。因爲(a)已知他們使用靜態數據類型;(b)他們調用malloc或free;(c)他們是標準I/O。標準I/O庫的很多實現都以不可重入方式使用全局數據結構。
6、函數kill和raise
kill函數將信號發送給進程或進程組,raise函數則允許進程向自身發送信號。
#include <signal.h>
int kill(pid_t pid,int signo);
int raise(int signo);
//兩個函數返回值:若成功,返回0;若出錯,返回-1.
調用raise(signo);相當於調用kill(getpid(),signo);
Kill的pid參數有以下4種不同的情況:
(1) pid>0 將該信號發送給進程ID爲pid的進程
(2) pid=0 將該信號發送給於發送進程屬於同一個進程組的所有進程
(3) pid<0 將該信號發送給其進程組ID等於pid絕對值,而且發送進程具有權限向其發送信號的所有進程
(4) pid=-1 將該進程發送給發送進程有權限向它們發送信號的所有進程。
其上所說所有進程不包括系統進程集中的進程。
7、函數alarm和pause
函數alarm設置一個定時器,當定時器超時時,產生SIGALRM信號。
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
//返回值:0或以前設置的鬧鐘時間的餘留秒數。
pause函數使調用進程掛起直至捕捉到一個信號
#include <signal.h>
int pause(void);
//返回值:-1,errno設置爲EINTR。
只有執行了一個信號處理程序並從其返回時,pause才返回。此時,pause返回-1,errno設置爲EINTR。
8、信號集
信號集是能表示多個信號的數據結構(sigset_t),下面列出5個處理信號集的函數
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set,int signo);
int sigdelset(sigset_t *set,int signo);
//四個函數返回值:若成功,返回0;若出錯,返回-1.
int siggismember(const sigset_t *set,int signo);
//返回值:若真,返回1;若假,返回0.
在使用信號集之前,要對該信號集進行初始化(調用sigemptyset或者sigfillset)。
9、函數sigprocmask
進程的信號屏蔽字規定了當前阻塞而不能遞送給該進程的信號集。調用函數sigprocmask可以檢測或更改進程的信號屏蔽字。
#include <signal.h>
int sigprocmask(int how,const sigset_t *restrict set,sigset_t *restrict oset);//返回值:若成功,返回0;若出錯,返回-1.
首先,若oset是非空指針,那麼進程的當前信號屏蔽字通過oset返回。其次,若set是一個非空指針,則參數how指示如何修改當前信號屏蔽字。SIG_BLOCK是或操作,SIG_SETMASK則是賦值操作。不能阻塞SIGKILL和SIGSTOP信號。如果set是空指針,則不改變該進程的信號屏蔽字。
10、函數sigpending
sigpending函數返回一信號集,對於調用進程而言,其中的各信號是阻塞不能遞送的,因而也一定是當前未決的。
#include <signal.h>
ing sigpending(sigset_t *set);
//返回值:若成功,返回0;若出錯,返回-1.
下面展示信號設置和sigprocmask實例
進程開始阻塞SIGQUIT信號,保存了當前信號屏蔽字(以便以後恢復),然後休眠5秒。在此期間所產生的退出信號SIGQUIT都被阻塞,不遞送至該進程。
5秒休眠後,檢查該信號是否是未決的,然後將SIGQUIT設置爲不再阻塞。
運行程序,在5s之內鍵入退出字符Ctril+\(產生SIGQUIT信號),然後在第二個5s之內再次鍵入退出字符。
11、 函數sigaction
sigaction函數的功能是檢查或修改與制定信號相關聯的處理動作。此函數取代了UNIX早期版本使用的signal函數。
#include <signal.h>
int sigaction(int signo,const struct sigction *restrict act,struct sigaction *restrict oact);//返回值:若成功,返回0;若出錯,返回-1.
參數signo是要檢測或修改其具體動作的信號編號。若act指針非空,則根據參數act修改其動作。若oact指針非空,則由oact指針返回該信號的上一個動作。
此函數使用下列結構:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
};
sa_handler字段包含一個信號捕捉函數的地址。
sa_mask字段說明了一個信號集,在調用該信號捕捉函數之前,這一信號集要加到進程的信號屏蔽字中。僅當從信號捕捉函數返回時將進程的信號屏蔽字恢復爲原先值。
sa_sigaction字段是一個替代的信號處理程序,當sa_flags設置爲SA_SIGINFO時,使用該信號處理程序。
sa_flags字段指定對信號進行處理的各個選項。
通常按下列方式調用信號處理程序:
void handler(int signo);
在設置了SA_SIGINFO標誌,那麼按下列凡是調用信號處理程序:
void handler(int signo,siginfo_t *info,void *context);
下面使用sigaction實現signal函數,它力圖阻止被中斷的系統調用重啓動
typedef void Sigfunc(int);
Sigfunc* mysignal(int signo,Sigfunc *func)
{
struct sigaction act,oact;
act.sa_handler = func;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
if(signo == SIGALRM)
{
#ifdef SA_INTERRUPT
act.sa_flags |= SA_INTERRUPT;
#endif
}
else
{
#ifdef SA_RESTART
act.sa_flags |= SA_RESTART;
#endif
}
if(sigaction(signo,&act,&oact)<0)
return (SIG_ERR);
return (oact.sa_handler);
}
12、 函數sigsetjmp和siglongjmp
之前說明了setjmp和longjmp函數可以用於非局部轉移,sigsetjmp跟siglongjmp指定了對信號屏蔽字的作用。
在信號處理程序中進行非局部轉移時應當使用這兩個函數。
#include <setjmp.h>
int sigsetjmp(sigjmp_buf env,int savemask);
//返回值:若直接調用,返回0;若從siglongjmp調用返回,則返回非0.
void siglongjmp(sigjmp_buf env,int val);
與setjmp和longjmp函數唯一的區別是sigsetjmp增加了一個參數savemask。如果savemask非0,則sigsetjmp在env中保存在env中保存進程的當前信號屏蔽字。調用siglongjmp時,從已經保存的env中恢復保存的信號屏蔽字。
13、函數sigsuspend
sigsuspend用於在接收到某個信號之前,臨時用sigmask替換進程的信號屏蔽字,並暫停進程執行,直到捕捉到一個信號而且從該信號處理程序返回,並且進程的信號屏蔽字設置爲調用sigsuspend之前的值。
#include <signal.h>
int sigsuspend(const sigset_t *sigmask);
//返回值:-1,並將errno設置爲EINTR。
進程的屏蔽字設置爲由sigmask指向的值。
下面顯示了保護代碼臨界區,使其不被特定信號中斷的正確方法
#include "apue.h"
static void sig_int(int);
int
main(void)
{
sigset_t newmask, oldmask, waitmask;
pr_mask("program start: ");
if (signal(SIGINT, sig_int) == SIG_ERR)
err_sys("signal(SIGINT) error");
sigemptyset(&waitmask);
sigaddset(&waitmask, SIGUSR1);
sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);
/*
* Block SIGINT and save current signal mask.
*/
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
err_sys("SIG_BLOCK error");
/*
* 代碼臨界區
*/
pr_mask("in critical region: ");
/*
* Pause, allowing all signals except SIGUSR1.
*/
if (sigsuspend(&waitmask) != -1)
err_sys("sigsuspend error");
pr_mask("after return from sigsuspend: ");
/*
* Reset signal mask which unblocks SIGINT.
*/
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
err_sys("SIG_SETMASK error");
/*
* And continue processing ...
*/
pr_mask("program exit: ");
exit(0);
}
static void
sig_int(int signo)
{
pr_mask("\nin sig_int: ");
}
下面是程序運行結果:
14、函數abort
abort函數的功能是使程序異常終止
#include <stdlib.h>
void abort(void);
此函數將SIGABRT信號發送給調用進程。讓進程捕捉SIGABRT信號目的是在進程終止之前由其執行所需的清理操作。默認情況是終止調用進程。
15、函數system
POSIX.1要求system函數忽略SIGINT和SITQUIT信號,阻塞SIGCHLD。
16、函數sleep
此函數使調用進程被掛起,直到滿足下列條件之一:
(1)已經經過seconds所指定的牆上時鐘時間。
(2)調用進程捕捉到一個信號並從信號處理程序返回。
#include<unistd.h>
unsigned int sleep(unsigned int seconds);
17、函數sigqueue
之前學過kill,raise,alarm,abort等功能稍簡單的信號發送函數,現在我們學習一種新的功能比較強大的信號發送函數sigqueue.
#include <sys/types.h>
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval val)
//調用成功返回 0;否則,返回 -1。
sigqueue()是比較新的發送信號系統調用,主要是針對實時信號提出的(當然也支持前32種),支持信號帶有參數,與函數sigaction()配合使用。
sigqueue的第一個參數是指定接收信號的進程ID,第二個參數確定即將發送的信號,第三個參數是一個聯合數據結構union sigval,指定了信號傳遞的參數,即通常所說的4字節值。
typedef union sigval {
int sival_int;
void *sival_ptr;
}sigval_t;
sigqueue()比kill()傳遞了更多的附加信息,但sigqueue()只能向一個進程發送信號,而不能發送信號給一個進程組。如果signo=0,將會執行錯誤檢查,但實際上不發送任何信號,0值信號可用於檢查pid的有效性以及當前進程是否有權限向目標進程發送信號。
在調用sigqueue時,sigval_t指定的信息會拷貝到對應sig 註冊的3參數信號處理函數的siginfo_t結構中,這樣信號處理函數就可以處理這些信息了。由於sigqueue系統調用支持發送帶參數信號,所以比kill()系統調用的功能要靈活和強大得多。
參考:http://www.cnblogs.com/runnyu/p/4641346.html
https://blog.csdn.net/men_wen/article/details/61204064
https://www.cnblogs.com/mickole/p/3191804.html