在之前的一篇文章中簡單分析了 Java 程序和 Linux 信號機制的處理。其中提到了 Linux 對信號的處理,但是感覺對這部分的理解還是不行,經過重新分析學習後,在這裏進行記錄。
基本前提
什麼是信號
首先看,什麼是信號:
信號(signal)是一種軟件中斷,它提供了一種處理異步事件的方法,也是進程間唯一的一步通信方式。
這個是《Linux C 編程實戰》的一段描述,要注意的就是不能直接說信號就是一種中斷,只能說信號的響應是依賴於中斷的。以時間片輪轉爲例,如果時間片結束時進程還在運行,那麼 CPU 將剝奪這個進程的執行並分配給另一個進程,那麼是如何知道“時間片結束”呢,這可以理解爲中斷的一種形式。我們還知道處理異步事件也可以使用線程,但是信號屬於初步異步,而線程屬於完全異步。
信號的種類
避免 OS 的影響,我在 CentOS 的虛擬機中輸入 kill -l
:
[root@localhost ~]# 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
編號 1~31 的信號都是屬於標準信號,繼承自 UNIX 系統,也是不可靠信號。編號 34~64 的信號,從名字上可以看出是“RT+MIN+X”和“RT+MAX-X”,這裏的 RT 就是 real time 的意思,即實時信號。
這裏截取了 UNIX 系統的部分信號:
可以看到默認動作最多的就是“終止”和“終止+core”,這裏的 core 指的就是在 Unix 系統下,應用程序崩潰產生的 core 文件,我們比較常見的“錯誤報告”其實就是一種 core 文件。
信號發生時,用戶可以設置以下三種方式對信號做出響應:
- 捕捉信號:對於要捕捉的信號,可以爲其指定信號處理函數,信號發生時該函數自動被調用,在該函數內部實現對該信號的處理
- 忽略信號:大多數信號都可使用這種方式進行處理,但是 SIGKILL 和 SIGSTOP 這兩個信號不能被忽略,同時這兩個信號也不能被捕獲和阻塞。此外,如果忽略某某些由硬件異常產生的信號(如非法存儲訪問或除以0),則進程的行爲是不可預測的。
- 按照系統默認方式處理。大部分信號的默認操作是終止進程,且所有的實時信號的默認動作都是終止進程。
signal 函數
signal
函數原型如下:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
void(* signal(int sig,void(* func)(int)))(int);
第一個參數是要處理的信號,當指定菜單信號到達時就會跳轉到 handler 指定的函數執行,如果 handler 不是函數指針,那麼還有兩種方式處理:
SIG_DFL
:由該特定信號的默認動作處理SIG_IGN
:忽略該信號,即使沒有意義,代碼執行仍將繼續
函數執行成功會返回以前的信號處理函數指針,執行錯誤會返回 SIG_ERR(-1)。
先看一個簡單的例子:
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
int main()
{
while (1){
sleep(1);
printf("Hello World\n");
};
//返回給 OS,0 是正常,非 0 是異常
return 0;
}
執行:
➜ ctemp ls
hello.c
➜ ctemp make hello
cc -I/usr/local/opt/openssl/include -L/usr/local/opt/openssl/lib hello.c -o hello
hello.c:8:7: warning: implicit declaration of function 'sleep' is invalid in C99 [-Wimplicit-function-declaration]
sleep(1);
^
1 warning generated.
➜ ctemp
➜ ctemp ls
hello hello.c
➜ ctemp ./hello
這時候會每隔 1s 打印"Hello World",此時如果在鍵盤輸入 <control+c>
:
➜ ctemp ./hello
Hello World
Hello World
Hello World
Hello World
^C
可以發現程序被終止了。結合上文中的信號表,能夠知道 SIGINT 信號的默認處理是終止,接下來使用 signal
函數修改一下:
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
int main()
{
while (1){
signal(SIGINT,SIG_IGN);
sleep(1);
printf("Hello World\n");
};
//返回給 OS,0 是正常,非 0 是異常
return 0;
}
再執行:
➜ ctemp make hello
cc -I/usr/local/opt/openssl/include -L/usr/local/opt/openssl/lib hello.c -o hello
hello.c:9:7: warning: implicit declaration of function 'sleep' is invalid in C99 [-Wimplicit-function-declaration]
sleep(1);
^
1 warning generated.
➜ ctemp ./hello
Hello World
Hello World
^C^CHello World
^C^C^C^C^C^CHello World
^CHello World
Hello World
Hello World
能夠發現此時已經忽略了 SIGINT
信號。再使用之前的一篇文章中的例子:
#include <stdio.h>
#include <signal.h>
void handler_sigint(int signo)
{
printf("Hello World\n");
}
int main()
{
//註冊
signal(SIGINT,handler_sigint);
while (1);
//返回給 OS,0 是正常,非 0 是異常
return 0;
}
執行:
➜ ctemp make hello2
cc -I/usr/local/opt/openssl/include -L/usr/local/opt/openssl/lib hello2.c -o hello2
➜ ctemp ./hello2
^CHello World
^CHello World
^CHello World
^CHello World
能夠看到此時程序在收到 SIGINT 信號的時候會響應輸出"Hello World"。
還有一個非常重要的就是信號會打斷阻塞的系統調用。先看一個例子:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
static void sig_handler(int s)
{
printf("Hello\n");
}
int main()
{
int i ;
signal(SIGINT,sig_handler);
for(i = 0 ; i < 10 ; i++)
{
write(1,"*",1);
sleep(1);
}
}
運行:
➜ ctemp make hello3
cc -I/usr/local/opt/openssl/include -L/usr/local/opt/openssl/lib hello3.c -o hello3
hello3.c:16:9: warning: implicit declaration of function 'write' is invalid in
C99 [-Wimplicit-function-declaration]
write(1,"*",1);
^
hello3.c:17:9: warning: implicit declaration of function 'sleep' is invalid in
C99 [-Wimplicit-function-declaration]
sleep(1);
^
2 warnings generated.
➜ ctemp ./hello3
****^CHello
*^CHello
*^CHello
*^CHello
*^CHello
*^CHello
*^CHello
理論上來說程序總共應該要運行 10s,每秒打印一個“*”,是如果我連續的在鍵盤輸入 <control+c>
,會發現程序雖然輸出了 10 個"*",但是程序總共運行的時間並沒有 10s,再看一個更直接的例子:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
static void sig_handler(int s)
{
printf("Hello\n");
}
char buf[10];
int main()
{
int i ;
signal(SIGINT,sig_handler);
for(i = 0 ; i < 10 ; i++)
{
write(1,"*",1);
sprintf(buf, "[%d]", sleep(3));
write(1,buf,3);
}
}
sleep
函數會返回剩餘秒數。運行:
➜ ctemp ./hello4
*^CHello
[2]*^CHello
[2]*^CHello
[3]*^CHello
[3]*^CHello
[3]*^CHello
[3]*^CHello
[3]*^CHello
[3]*^CHello
[3]*^CHello
[3]
可以看到會出現壓根就沒等待的情況。很多阻塞函數都會被信號打斷,比如 read
函數:
EINTR While blocked waiting to complete an open of a slow device (e.g., a FIFO; see fifo(7)),the call was interrupted by a signal handler; see signal(7).
References
- 《Linux C 編程實戰》
- 《UNIX 環境高級編程(第3版)》
歡迎關注公衆號