概述
Linux 在進程間通信時,有時候需要用到異步通訊方式,而信號機制是Linux系統(tǒng)本身提供的一種異步通訊.
Linux中信號的類別
Linux信號在系統(tǒng)中總數(shù)是有限的,信號種類如下所示:
這些信號在Linux系統(tǒng)中各自有不同的用途.同時在Unix和Linux的不斷發(fā)展中出現(xiàn)了兩種信號,或者說由于歷史遺留問題出現(xiàn)了兩種信號:可靠信號和不可靠信號.不可靠信號是從早期的Unix繼承而來,而可靠信號是后來定義的信號.
信號的處理
一個進程對信號的響應可以分為三種情況,分別為:
- 忽略信號
- 捕捉信號
- 執(zhí)行系統(tǒng)默認操作
忽略信號
忽略信號是指在代碼中進行設置后,進程不會對信號進行響應,值得注意的是有兩個信號是不能忽略的這兩個信號為SIGKILL和SIGSTOP.
捕捉信號
捕捉信號是指在代碼中設置函數(shù),當指定的信號發(fā)生時,調用已經設置好的處理函數(shù),使得進程可以按照自己的意愿對信號發(fā)生時所代表的時間進行處理.
執(zhí)行系統(tǒng)默認操作
Linux操作系統(tǒng)規(guī)定了很多對于信號的默認操作,這些可以通過查詢獲取到,但是對于實時信號來說,器系統(tǒng)的默認操作都是進程終止.
信號的使用
在使用信號時首先需要確認使用何種信號.然后需要進程去產生信號.
信號的產生
信號可以通過六個函數(shù)產生:
- kill函數(shù)
- raise函數(shù)
- sigqueue函數(shù)
- alarm函數(shù)
- setitimer函數(shù)
- abort函數(shù)
kill函數(shù)
kill函數(shù)原型如下:
int kill(pid_t pid,int signo)
kill函數(shù)中的pid參數(shù)可以設置為如下方式:
pid > 0: 將信號發(fā)送給指定進程ID為pid的進程
pid == 0: 將信號發(fā)送給與發(fā)送進程在同一進程組的所有進程
pid < 0: 將信號發(fā)送給進程組ID等于pid絕對值的進程,如果pid==-1,那么就將信 號發(fā)送給有權限發(fā)送信號的系統(tǒng)上的所有進程.
kill函數(shù)中的signo參數(shù)也可以設置為如下方式:
signo == 0:發(fā)送一個空信號,實際上不發(fā)送任何值給目標進程,但是可以檢測目標進程是否存在,同時是否有權限向目標進程發(fā)送該信號.
signo != 0:向目標進程發(fā)送指定的信號
raise函數(shù)
raise函數(shù)原型如下:
int raise(int sig);
raise函數(shù)在實質上等價于kill(get_pid(),signo);因此raise函數(shù)只可以向自身進程發(fā)送信號其中signo參數(shù)的設置和kill函數(shù)相同.
sigqueue函數(shù)
sigqueue函數(shù)原型如下:
int sigqueue(pid_t pid, int sig, const union sigval value);
sigqueue函數(shù)的pid參數(shù)和sig參數(shù)和kill函數(shù)相同其功能也和kill函數(shù)類似.但是和kill函數(shù)不同sigqueue函數(shù)是較新的發(fā)送信號的函數(shù),支持后面出現(xiàn)的實時信號,在發(fā)送信號的時候,也支持參數(shù)的傳遞,比kill函數(shù)多了一些信號的附加信息.
sigqueue函數(shù)比kill函數(shù)更加優(yōu)越的地方主要在于第三個參數(shù)的使用上.第三個參數(shù)定義如下:
typedef union sigval {
int sival_int;
void *sival_ptr;
}sigval_t;
可以注意到這是一個聯(lián)合體,在聯(lián)合體中可以指定信號傳輸?shù)膮?shù),要么是一個四字節(jié)值,要么是一個指針.使用這個聯(lián)合體時,信號的目標進程也需要使用新的信號捕捉函數(shù)sigaction,否則該參數(shù)無效,具體的內容參照下面關于sigaction函數(shù)的敘述.
alarm函數(shù)
alarm函數(shù)的原型如下所示:
unsigned int alarm(unsigned int seconds);
信號中有一個專門用來定時的信號SIGALRM信號,alarm函數(shù)就是為使用這個信號專門設計的一個函數(shù),在alarm函數(shù)中的senconds參數(shù)中傳入具體的秒數(shù),在相應的時間到達時,就會向函數(shù)所在進程發(fā)送一個SIGALRM信號.
需要注意的一點是,每個進程只能擁有一個鬧鐘時間,如果一個進程已經設置過鬧鐘時間,且時間還未達到時,再次調用alarm函數(shù)設置鬧鐘時間,那么之前的值將會被新值替代,同時將鬧鐘時間的余留值返回.所以當新設置的鬧鐘時間為0時,就會取消原有的鬧鐘時間.
setitimer函數(shù)
setitimer函數(shù)原型如下:
int setitimer(int which, const struct itimerval *new_value,struct itimerval *old_value);
setitimer函數(shù)是對alarm函數(shù)功能的擴充,同時setitimer函數(shù)還有一個配套的查詢函數(shù):
int getitimer(int which, struct itimerval *curr_value);
setitimer函數(shù)支持三種定時器,這個選擇由which參數(shù)指定,這三個定時器分別為:
- ITIMER_REAL:設定絕對時間,當設定的時間到達時內核將發(fā)送SIGALRM信號給本進程
- ITIMER_VIRTUAL :設定程序的執(zhí)行時間(指程序在用戶層運行的時間),當程序的執(zhí)行時間到達時,內核將發(fā)送SIGALRM信號給本進程
- ITIMER_PROF :設定進程執(zhí)行以及內核因本進程而消耗的時間總和,內核將發(fā)送ITIMER_VIRTUAL信號給本進程
setitimer的第二個參數(shù)是指定運行的時間,這個參數(shù)的結構體原型如下所示:
struct itimerval
{
struct timeval it_interval; /* Interval for periodic timer */
struct timeval it_value; /* Time until next expiration */
};
struct timeval
{
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
其中itimerval結構體是傳入的參數(shù),這個結構體包含了兩個timerval結構體變量,這兩個變量用來設定時間.
其中it_interval指定的是發(fā)送信號的周期時間,it_value中保存的是到下一次發(fā)送信號的時間.
在setitimer的第三個參數(shù)時返回之前設定的時間周期值.
abort函數(shù)
abort函數(shù)原型如下
void abort(void);
該函數(shù)向進程發(fā)送SIGABORT信號,默認情況下進程會異常退出饱溢,即使SIGABORT被進程設置為阻塞信號帅刊,調用abort()后窟她,SIGABORT仍然能被進程接收陈症。該函數(shù)無返回值。
信號的捕捉和處理函數(shù)
目前在Linux中信號的捕捉處理函數(shù)有兩個:
- signal函數(shù)
- sigaction函數(shù)
signal函數(shù)
signal函數(shù)的原型如下所示i:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
第一個參數(shù)signum負責設定相應要捕捉的信號
第二個參數(shù)是一個函數(shù)指針,這個參數(shù)可以被指定為3個值:
SIG_IGN:忽略該信號
SIG_DFL:系統(tǒng)默認方式處理信號
函數(shù)指針:負責設定捕捉到信號時應采取的操作,函數(shù)原型為typedef指定的格式.
sigaction函數(shù)
sigaction函數(shù)在功能上已經徹底取代了signal函數(shù),該函數(shù)的原型為:
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
第一個參數(shù)signum負責指定要捕捉的信號.
第二個參數(shù)和第三個參數(shù)都是一個sigaction結構體,其中第二個為設定新值,第一個為返回原有的設定值.該結構體定義如下:
struct sigaction
{
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
注意,在使用過程中,第一個元素sa_handler和第二個元素sa_sigaction不能同時指定.
這個結構體中的元素如下:
- 第一個元素sa_handler是函數(shù)指針:(可以參照signal函數(shù)中的函數(shù)指針)
負責指定關于信號的信號處理函數(shù),但是該函數(shù)只能傳入一個對應信號的信號值 - 第二個參數(shù)sa_sigaction也是一個函數(shù)指針:(可以參照signal函數(shù)中的函數(shù)指針)
負責指定相應信號的處理函數(shù),但是該函數(shù)可以傳入三個參數(shù),第一個為信號值,第二個參數(shù)為一個siginfo_t結構體用以說明本次信號處理的各種詳細信息,第三個參數(shù)現(xiàn)行標準中現(xiàn)在還沒有做相關規(guī)定
siginfo_t結構體定義如下:
siginfo_t
{
int si_signo; /* Signal number */
int si_errno; /* An errno value */
int si_code; /* Signal code */
int si_trapno; /* Trap number that caused
hardware-generated signal
(unused on most architectures) */
pid_t si_pid; /* Sending process ID */
uid_t si_uid; /* Real user ID of sending process */
int si_status; /* Exit value or signal */
clock_t si_utime; /* User time consumed */
clock_t si_stime; /* System time consumed */
sigval_t si_value; /* Signal value */
int si_int; /* POSIX.1b signal */
void *si_ptr; /* POSIX.1b signal */
int si_overrun; /* Timer overrun count;
POSIX.1b timers */
int si_timerid; /* Timer ID; POSIX.1b timers */
void *si_addr; /* Memory location which caused fault */
long si_band; /* Band event (was int in
glibc 2.3.2 and earlier) */
int si_fd; /* File descriptor */
short si_addr_lsb; /* Least significant bit of address
(since Linux 2.6.32) */
void *si_call_addr; /* Address of system call instruction
(since Linux 3.5) */
int si_syscall; /* Number of attempted system call
(since Linux 3.5) */
unsigned int si_arch; /* Architecture of attempted system call
(since Linux 3.5) */
}
第三個元素是一個信號集:設定了在信號處理期間應該屏蔽的信號.
第四個元素:設定了修改信號行為的標識.例如當自身子進程狀態(tài)改變時,不接受子進程狀態(tài)改變信號等.這個flag的設定值可以通過查詢獲取到.
第五個參數(shù)也是一個函數(shù)指針:POSIX標準不再使用.
信號處理中的一些問題
- 1 信號SIGKILL和SIGSTOP不能被忽略,如果采取忽略這兩個信號,那么程序會報錯
- 2 對某個進程發(fā)送信號時,如果該進程是多線程的,就會出現(xiàn)問題.由于信號在設計時沒有多線程概念,所以在設計之初就沒有考慮信號和多線程一起使用的情況,雖然在后面對標準進行了重新修訂,單一些較老的信號依然存在一些問題.
在信號和多線程一起使用時,信號只會被進程中的一個線程處理,其他線程是無法收到信號的.而且處理信號的線程是未知的(在ubuntu下,測試發(fā)現(xiàn)信號首先被主線程處理),而如果指定的信號沒有被處理信號的線程重新定義操作函數(shù),那么會采用系統(tǒng)默認操作,導致程序運行出現(xiàn)問題.最好的解決辦法就是,其他線程都屏蔽要捕捉的線程,只有信號處理線程不進行屏蔽,那么該信號就會達到目標線程進行處理.
對信號阻塞進行處理的函數(shù)如下所示:
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
其中how共有三個參數(shù):
how的取值為: - SIG_BLOCK 把參數(shù)set中的信號添加到信號屏蔽字中
- SIG_SETMASK 把信號屏蔽字設置為參數(shù)set中的信號
-
SIG_UNBLOCK 從信號屏蔽字中刪除參數(shù)set中的信號
而在使用sigset之前要確保先調用int sigemptyset(sigset_t *set)或者int sigfillset(sigset_t *set); 對信號集進行初始化,否則該參數(shù)初值是未知的.
sigemptyset(sigset_t *set)初始化由set指定的信號集震糖,信號集里面的所有信號被清空;
int sigfillset(sigset_t *set);
調用該函數(shù)后录肯,set指向的信號集中將包含linux支持的64種信號;