一 信號的種類
可靠信號與不可靠信號, 實時信號與非實時信號
可靠信號就是實時信號, 那些從UNIX系統繼承過來的信號都是非可靠信號, 表現在信號
不支持排隊,信號可能會丟失, 比如發送多次相同的信號, 進程只能收到一次. 信號值小于
SIGRTMIN的都是非可靠信號.
非可靠信號就是非實時信號, 后來, Linux改進了信號機制, 增加了32種新的信號, 這些信
號都是可靠信號, 表現在信號支持排隊, 不會丟失, 發多少次, 就可以收到多少次. 信號值
位于 [SIGRTMIN, SIGRTMAX] 區間的都是可靠信號.
關于可靠信號, 還可以參考WIKI的一段話:
Text代碼

- The real-time signals, ranging from SIGRTMIN to SIGRTMAX, are a set of signals that can be used for application-defined purposes.
- Because SIGRTMIN may have different values on different Unix-like systems, applications should always refer to the signals in the form SIGRTMIN+n, where n is a constant integer expression.
- The real-time signals have a number of properties that differentiate them from other signals and make them suitable for application-defined purposes:
- * Multiple instances of a real-time signal can be sent to a process and all will be delivered.
- * Real-time signals can be accompanied by an integer or pointer value (see sigqueue[2]).
- * Real-time signals are guaranteed to be delivered in the order they were emitted.
命令行輸入 kill -l, 可以列出系統支持的所有信號:
C代碼

- ~> kill -l
- 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
- 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
- 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
- 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
- 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
- 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
- 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
- 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
- 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
- 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
- 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
- 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
- 63) SIGRTMAX-1 64) SIGRTMAX
非可靠信號一般都有確定的用途及含義, 可靠信號則可以讓用戶自定義使用
二 信號的安裝
早期的Linux使用系統調用 signal 來安裝信號
#include <signal.h>
void (*signal(int signum, void (*handler))(int)))(int);
該函數有兩個參數, signum指定要安裝的信號, handler指定信號的處理函數.
該函數的返回值是一個函數指針, 指向上次安裝的handler
經典安裝方式:
if (signal(SIGINT, SIG_IGN) != SIG_IGN) {
signal(SIGINT, sig_handler);
}
先獲得上次的handler, 如果不是忽略信號, 就安裝此信號的handler
由于信號被交付后, 系統自動的重置handler為默認動作, 為了使信號在handler
處理期間, 仍能對后繼信號做出反應, 往往在handler的第一條語句再次調用 signal
sig_handler(ing signum)
{
/* 重新安裝信號 */
signal(signum, sig_handler);
......
}
我們知道在程序的任意執行點上, 信號隨時可能發生, 如果信號在sig_handler重新安裝
信號之前產生, 這次信號就會執行默認動作, 而不是sig_handler. 這種問題是不可預料的.
使用庫函數 sigaction 來安裝信號
為了克服非可靠信號并同一SVR4和BSD之間的差異, 產生了 POSIX 信號安裝方式, 使用
sigaction安裝信號的動作后, 該動作就一直保持, 直到另一次調用 sigaction建立另一個
動作為止. 這就克服了古老的 signal 調用存在的問題
#include <signal.h>
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));
經典安裝方式:
struct sigaction action, old_action;
/* 設置SIGINT */
action.sa_handler = sig_handler;
sigemptyset(&action.sa_mask);
sigaddset(&action.sa_mask, SIGTERM);
action.sa_flags = 0;
/* 獲取上次的handler, 如果不是忽略動作, 則安裝信號 */
sigaction(SIGINT, NULL, &old_action);
if (old_action.sa_handler != SIG_IGN) {
sigaction(SIGINT, &action, NULL);
}
基于 sigaction 實現的庫函數: signal
sigaction 自然強大, 但安裝信號很繁瑣, 目前linux中的signal()是通過sigation()函數
實現的,因此,即使通過signal()安裝的信號,在信號處理函數的結尾也不必
再調用一次信號安裝函數。
三 如何屏蔽信號
所謂屏蔽, 并不是禁止遞送信號, 而是暫時阻塞信號的遞送,
解除屏蔽后, 信號將被遞送, 不會丟失. 相關API為
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
int sigsuspend(const sigset_t *mask);
int sigpending(sigset_t *set);
-----------------------------------------------------------------
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset));
sigprocmask()函數能夠根據參數how來實現對信號集的操作,操作主要有三種:
* SIG_BLOCK 在進程當前阻塞信號集中添加set指向信號集中的信號
* SIG_UNBLOCK 如果進程阻塞信號集中包含set指向信號集中的信號,則解除
對該信號的阻塞
* SIG_SETMASK 更新進程阻塞信號集為set指向的信號集
屏蔽整個進程的信號:
C代碼

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <error.h>
#include <string.h>
void sig_handler(int signum)
{
printf("catch SIGINT\n");
}
int main(int argc, char **argv)
{
sigset_t block;
struct sigaction action, old_action;
/* 安裝信號 */
action.sa_handler = sig_handler;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
sigaction(SIGINT, NULL, &old_action);
if (old_action.sa_handler != SIG_IGN) {
sigaction(SIGINT, &action, NULL);
}
/* 屏蔽信號 */
sigemptyset(&block);
sigaddset(&block, SIGINT);
printf("block SIGINT\n");
sigprocmask(SIG_BLOCK, &block, NULL);
printf("--> send SIGINT -->\n");
kill(getpid(), SIGINT);
printf("--> send SIGINT -->\n");
kill(getpid(), SIGINT);
sleep(1);
/* 解除信號后, 之前觸發的信號將被遞送,
* 但SIGINT是非可靠信號, 只會遞送一次
*/
printf("unblock SIGINT\n");
sigprocmask(SIG_UNBLOCK, &block, NULL);
sleep(2);
return 0;
}
運行結果:
C代碼

work> ./a.out
block SIGINT
--> send SIGINT -->
--> send SIGINT -->
unblock SIGINT
catch SIGINT
這里發送了兩次SIGINT信號 可以看到, 屏蔽掉SIGINT后,
信號無法遞送, 解除屏蔽后, 才遞送信號, 但只被遞送一次,
因為SIGINT是非可靠信號, 不支持排隊.
只在信號處理期間, 屏蔽其它信號
在信號的handler執行期間, 系統將自動屏蔽此信號, 但如果
還想屏蔽其它信號怎么辦? 可以利用 struct sigaction 結構體
的 sa_mask 屬性.
C代碼

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <error.h>
#include <string.h>
void sig_handler(int signum)
{
printf("in handle, SIGTERM is blocked\n");
/* 在此handler內將屏蔽掉SIGTERM, 直到此handler返回 */
printf("--> send SIGTERM -->\n");
kill(getpid(), SIGTERM);
sleep(5);
printf("handle done\n");
}
void handle_term(int signum)
{
printf("catch sigterm and exit..\n");
exit(0);
}
int main(int argc, char **argv)
{
struct sigaction action, old_action;
/* 設置SIGINT */
action.sa_handler = sig_handler;
sigemptyset(&action.sa_mask);
/* 安裝handler的時候, 設置在handler
* 執行期間, 屏蔽掉SIGTERM信號 */
sigaddset(&action.sa_mask, SIGTERM);
action.sa_flags = 0;
sigaction(SIGINT, NULL, &old_action);
if (old_action.sa_handler != SIG_IGN) {
sigaction(SIGINT, &action, NULL);
}
/* 設置SIGTERM */
action.sa_handler = handle_term;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
sigaction(SIGTERM, NULL, &old_action);
if (old_action.sa_handler != SIG_IGN) {
sigaction(SIGTERM, &action, NULL);
}
printf("--> send SIGINT -->\n");
kill(getpid(), SIGINT);
while (1) {
sleep(1);
}
return 0;
}
運行結果:
C代碼

work> ./a.out
--> send SIGINT -->
in handle, SIGTERM is blocked
--> send SIGTERM -->
handle done
catch sigterm and exit..
收到SIGINT后, 進入sig_handler,此時發送SIGTERM信號將被屏蔽,
等sig_handler返回后, 才收到SIGTERM信號, 然后退出程序
四 如何獲取未決信號
所謂未決信號, 是指被阻塞的信號, 等待被遞送的信號.
int sigsuspend(const sigset_t *mask));
sigpending(sigset_t *set))獲得當前已遞送到進程,
卻被阻塞的所有信號,在set指向的信號集中返回結果。
C代碼

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <error.h>
#include <string.h>
/* 版本1, 可靠信號將被遞送多次 */
//#define MYSIGNAL SIGRTMIN+5
/* 版本2, 不可靠信號只被遞送一次 */
#define MYSIGNAL SIGTERM
void sig_handler(int signum)
{
psignal(signum, "catch a signal");
}
int main(int argc, char **argv)
{
sigset_t block, pending;
int sig, flag;
/* 設置信號的handler */
signal(MYSIGNAL, sig_handler);
/* 屏蔽此信號 */
sigemptyset(&block);
sigaddset(&block, MYSIGNAL);
printf("block signal\n");
sigprocmask(SIG_BLOCK, &block, NULL);
/* 發兩次信號, 看信號將會被觸發多少次 */
printf("---> send a signal --->\n");
kill(getpid(), MYSIGNAL);
printf("---> send a signal --->\n");
kill(getpid(), MYSIGNAL);
/* 檢查當前的未決信號 */
flag = 0;
sigpending(&pending);
for (sig = 1; sig < NSIG; sig++) {
if (sigismember(&pending, sig)) {
flag = 1;
psignal(sig, "this signal is pending");
}
}
if (flag == 0) {
printf("no pending signal\n");
}
/* 解除此信號的屏蔽, 未決信號將被遞送 */
printf("unblock signal\n");
sigprocmask(SIG_UNBLOCK, &block, NULL);
/* 再次檢查未決信號 */
flag = 0;
sigpending(&pending);
for (sig = 1; sig < NSIG; sig++) {
if (sigismember(&pending, sig)) {
flag = 1;
psignal(sig, "a pending signal");
}
}
if (flag == 0) {
printf("no pending signal\n");
}
return 0;
}
這個程序有兩個版本:
可靠信號版本, 運行結果:
C代碼

work> ./a.out
block signal
---> send a signal --->
---> send a signal --->
this signal is pending: Unknown signal 39
unblock signal
catch a signal: Unknown signal 39
catch a signal: Unknown signal 39
no pending signal
發送兩次可靠信號, 最終收到兩次信號
非可靠信號版本, 運行結果:
C代碼

work> ./a.out
block signal
---> send a signal --->
---> send a signal --->
this signal is pending: Terminated
unblock signal
catch a signal: Terminated
no pending signal
發送兩次非可靠信號, 最終只收到一次
五 被中斷了的系統調用
一些IO系統調用執行時, 如 read 等待輸入期間, 如果收到一個信號,
系統將中斷read, 轉而執行信號處理函數. 當信號處理返回后, 系統
遇到了一個問題: 是重新開始這個系統調用, 還是讓系統調用失敗?
早期UNIX系統的做法是, 中斷系統調用, 并讓系統調用失敗, 比如read
返回 -1, 同時設置 errno 為 EINTR
中斷了的系統調用是沒有完成的調用, 它的失敗是臨時性的, 如果再次調用
則可能成功, 這并不是真正的失敗, 所以要對這種情況進行處理, 典型的方式為:
while (1) {
n = read(fd, buf, BUFSIZ);
if (n == -1 && errno != EINTR) {
printf("read error\n");
break;
}
if (n == 0) {
printf("read done\n");
break;
}
}
這樣做邏輯比較繁瑣, 事實上, 我們可以從信號的角度
來解決這個問題, 安裝信號的時候, 設置 SA_RESTART
屬性, 那么當信號處理函數返回后, 被該信號中斷的系統
調用將自動恢復.
C代碼

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <error.h>
#include <string.h>
void sig_handler(int signum)
{
printf("in handler\n");
sleep(1);
printf("handler return\n");
}
int main(int argc, char **argv)
{
char buf[100];
int ret;
struct sigaction action, old_action;
action.sa_handler = sig_handler;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
/* 版本1:不設置SA_RESTART屬性
* 版本2:設置SA_RESTART屬性 */
//action.sa_flags |= SA_RESTART;
sigaction(SIGINT, NULL, &old_action);
if (old_action.sa_handler != SIG_IGN) {
sigaction(SIGINT, &action, NULL);
}
bzero(buf, 100);
ret = read(0, buf, 100);
if (ret == -1) {
perror("read");
}
printf("read %d bytes:\n", ret);
printf("%s\n", buf);
return 0;
}
版本1, 不設置 SA_RESTART 屬性 :
C代碼

work> gcc signal.c
work> ./a.out
^Cin handler
handler return
read: Interrupted system call
read -1 bytes:
在 read 等待數據期間, 按下ctrl + c, 觸發 SIGINT 信號,
handler 返回后, read 被中斷, 返回 -1
版本2, 設置 SA_RESTART 屬性:
C代碼

work> gcc signal.c
work> ./a.out
^Cin handler
handler return
hello, world
read 13 bytes:
hello, world
handler 返回后, read 系統調用被恢復執行, 繼續等待數據.
六 非局部控制轉移
int setjmp(jmp_buf env);
int sigsetjmp(sigjmp_buf env, int savesigs);
void longjmp(jmp_buf env, int val);
void siglongjmp(sigjmp_buf env, int val);
--------------------------------------------------------
setjmp()會保存目前堆棧環境,然后將目前的地址作一個記號,
而在程序其他地方調用 longjmp 時便會直接跳到這個記號位置,
然后還原堆棧,繼續程序好執行。
setjmp調用有點fork的味道, setjmp()return 0 if returning directly,
and non-zero when returning from longjmp using the saved context.
if (setjmp(jmpbuf)) {
printf("return from jmp\n");
} else {
printf("return directly\n");
}
setjmp 和 sigsetjmp 的唯一區別是: setjmp 不一定會恢復信號集合,
而sigsetjmp可以保證恢復信號集合
C代碼

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <setjmp.h>
void sig_alrm(int signum);
void sig_usr1(int signum);
void print_mask(const char *str);
static sigjmp_buf jmpbuf;
static volatile sig_atomic_t canjmp;
static int sigalrm_appear;
int main(int argc, char **argv)
{
struct sigaction action, old_action;
/* 設置SIGUSR1 */
action.sa_handler = sig_usr1;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
sigaction(SIGUSR1, NULL, &old_action);
if (old_action.sa_handler != SIG_IGN) {
sigaction(SIGUSR1, &action, NULL);
}
/* 設置SIGALRM */
action.sa_handler = sig_alrm;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
sigaction(SIGALRM, NULL, &old_action);
if (old_action.sa_handler != SIG_IGN) {
sigaction(SIGALRM, &action, NULL);
}
print_mask("starting main:");
if (sigsetjmp(jmpbuf, 1) != 0) {
print_mask("exiting main:");
} else {
printf("sigsetjmp return directly\n");
canjmp = 1;
while (1) {
sleep(1);
}
}
return 0;
}
void sig_usr1(int signum)
{
time_t starttime;
if (canjmp == 0) {
printf("please set jmp first\n");
return;
}
print_mask("in sig_usr1:");
alarm(1);
while (!sigalrm_appear);
canjmp = 0;
siglongjmp(jmpbuf, 1);
}
void sig_alrm(int signum)
{
print_mask("in sig_alrm:");
sigalrm_appear = 1;
return;
}
void print_mask(const char *str)
{
sigset_t sigset;
int i, errno_save, flag = 0;
errno_save = errno;
if (sigprocmask(0, NULL, &sigset) < 0) {
printf("sigprocmask error\n");
exit(0);
}
printf("%s\n", str);
fflush(stdout);
for (i = 1; i < NSIG; i++) {
if (sigismember(&sigset, i)) {
flag = 1;
psignal(i, "a blocked signal");
}
}
if (!flag) {
printf("no blocked signal\n");
}
printf("\n");
errno = errno_save;
}
運行結果:
C代碼

work> ./a.out &
[4] 28483
starting main:
no blocked signal
sigsetjmp return directly
kill -USR1 28483
in sig_usr1:
a blocked signal: User defined signal 1
in sig_alrm:
a blocked signal: User defined signal 1
a blocked signal: Alarm clock
exiting main:
no blocked signal
七 信號的生命周期
從信號發送到信號處理函數的執行完畢
對于一個完整的信號生命周期(從信號發送到相應的處理函數執行完畢)來說,
可以分為三個重要的階段,這三個階段由四個重要事件來刻畫:
信號誕生;信號在進程中注冊完畢;信號在進程中的注銷完畢;信號處理函數執行完畢。
下面闡述四個事件的實際意義:
信號"誕生"。信號的誕生指的是觸發信號的事件發生
(如檢測到硬件異常、定時器超時以及調用信號發送函數
kill()或sigqueue()等)。信號在目標進程中"注冊";
進程的task_struct結構中有關于本進程中未決信號的數據成員:
struct sigpending pending:
struct sigpending{
struct sigqueue *head, **tail;
sigset_t signal;
};
第三個成員是進程中所有未決信號集,第一、第二個成員分別指向一個
sigqueue類型的結構鏈(稱之為"未決信號鏈表")的首尾,鏈表中
的每個sigqueue結構刻畫一個特定信號所攜帶的信息,并指向下一個
sigqueue結構:
struct sigqueue{
struct sigqueue *next;
siginfo_t info;
}
信號的注冊
信號在進程中注冊指的就是信號值加入到進程的未決信號集中
(sigpending結構的第二個成員sigset_t signal),
并且加入未決信號鏈表的末尾。 只要信號在進程的未決信號集中,
表明進程已經知道這些信號的存在,但還沒來得及處理,或者該信號被進程阻塞。
當一個實時信號發送給一個進程時,不管該信號是否已經在進程中注冊,
都會被再注冊一次,因此,信號不會丟失,因此,實時信號又叫做"可靠信號"。
這意味著同一個實時信號可以在同一個進程的未決信號鏈表中添加多次.
當一個非實時信號發送給一個進程時,如果該信號已經在進程中注冊,
則該信號將被丟棄,造成信號丟失。因此,非實時信號又叫做"不可靠信號"。
這意味著同一個非實時信號在進程的未決信號鏈表中,至多占有一個sigqueue結構.
一個非實時信號誕生后,
(1)、如果發現相同的信號已經在目標結構中注冊,則不再注冊,對于進程來說,
相當于不知道本次信號發生,信號丟失.
(2)、如果進程的未決信號中沒有相同信號,則在進程中注冊自己。
信號的注銷。
在進程執行過程中,會檢測是否有信號等待處理
(每次從系統空間返回到用戶空間時都做這樣的檢查)。如果存在未決
信號等待處理且該信號沒有被進程阻塞,則在運行相應的信號處理函數前,
進程會把信號在未決信號鏈中占有的結構卸掉。是否將信號從進程未決信號集
中刪除對于實時與非實時信號是不同的。對于非實時信號來說,由于在未決信
號信息鏈中最多只占用一個sigqueue結構,因此該結構被釋放后,應該把信
號在進程未決信號集中刪除(信號注銷完畢);而對于實時信號來說,可能在
未決信號信息鏈中占用多個sigqueue結構,因此應該針對占用sigqueue結構
的數目區別對待:如果只占用一個sigqueue結構(進程只收到該信號一次),
則應該把信號在進程的未決信號集中刪除(信號注銷完畢)。否則,不應該在進程
的未決信號集中刪除該信號(信號注銷完畢)。
進程在執行信號相應處理函數之前,首先要把信號在進程中注銷。
信號生命終止。
進程注銷信號后,立即執行相應的信號處理函數,執行完畢后,
信號的本次發送對進程的影響徹底結束。
八 關于可重入函數
在信號處理函數中應使用可重入函數。
信號處理程序中應當使用可重入函數
(注:所謂可重入函數是指一個可以被多個任務調用的過程,
任務在調用時不必擔心數據是否會出錯)。因為進程在收到信號
后,就將跳轉到信號處理函數去接著執行。如果信號處理函數中
使用了不可重入函數,那么信號處理函數可能會修改原來進程中
不應該被修改的數據,這樣進程從信號處理函數中返回接著執行時,
可能會出現不可預料的后果。不可再入函數在信號處理函數中被視為
不安全函數。滿足下列條件的函數多數是不可再入的:
(1)使用靜態的數據結構,如getlogin(),gmtime(),getgrgid(),
getgrnam(),getpwuid()以及getpwnam()等等;
(2)函數實現時,調用了malloc()或者free()函數;
(3)實現時使用了標準I/O函數的。The Open Group視下列函數為可再入的:
_exit()、access()、alarm()、cfgetispeed()、cfgetospeed()、
cfsetispeed()、cfsetospeed()、chdir()、chmod()、chown() 、
close()、creat()、dup()、dup2()、execle()、execve()、
fcntl()、fork()、fpathconf()、fstat()、fsync()、getegid()、
geteuid()、getgid()、getgroups()、getpgrp()、getpid()、
getppid()、getuid()、kill()、link()、lseek()、mkdir()、
mkfifo()、 open()、pathconf()、pause()、pipe()、raise()、
read()、rename()、rmdir()、setgid()、setpgid()、setsid()、
setuid()、 sigaction()、sigaddset()、sigdelset()、sigemptyset()、
sigfillset()、sigismember()、signal()、sigpending()、
sigprocmask()、sigsuspend()、sleep()、stat()、sysconf()、
tcdrain()、tcflow()、tcflush()、tcgetattr()、tcgetpgrp()、
tcsendbreak()、tcsetattr()、tcsetpgrp()、time()、times()、
umask()、uname()、unlink()、utime()、wait()、waitpid()、
write()。
即使信號處理函數使用的都是"安全函數",同樣要注意進入處理函數時,
首先要保存errno的值,結束時,再恢復原值。因為,信號處理過程中,
errno值隨時可能被改變。另外,longjmp()以及siglongjmp()沒有被列為可重入函數,
因為不能保證緊接著兩個函數的其它調用是安全的。