Linux C 編程基礎之信號機制(一)

之前的一篇文章中簡單分析了 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版)》

歡迎關注公衆號
​​​​​​在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章