天天看點

作業系統用信号控制程式

程序之死

在linux的終端正在運作的程式,使用者按了ctrl-C,程式就停止運作了。為什麼ctrl-C可以徹底殺死程式?是因為當作業系統從鍵盤讀取資料時,發現使用者按了ctrl-C時,就會向程式發送中斷信号。

信号是一條短消息,即一個整型值。當信号到來時,程序必須停止手中一切工作去處理信号。程序會檢視信号映射表,表中每個信号都對應一個信号處理器函數。中斷信号的預設信号處理器會調用exit()函數。

信号映射表

信号 處理函數
SIGURG 不做事情
SIGINT 調用exit()

作業系統為什麼不直接結束程式,而是要在信号映射表中查找信号?因為這樣就可以在程序接收信号時運作你自己的代碼。我隻要想辦法把信号對應的預設處理器函數換成我們自己的,我們就可以達到捕捉信号後,根據信号運作我們自己的代碼的目的了。

捕捉信号,然後運作自己的代碼

有時候,在程序打開了一些檔案連接配接或網絡連接配接,我們希望在退出之前把它們關閉,并且做一些清理工作。我們可以這樣做,當計算機向我們的程式發送信号時,我們捕捉相應的信号,然後根據信号做相應的處理。那怎麼實作呢?答案就是用sigaction結構體包裝一個處理器函數,然後用sigaction()函數進行信号值與處理器函數的綁定。

sigaction是一個函數包裝器,就是說sigaction一個結構體,它裡面有一個函數指針變量,我們将處理器函數指針賦給它即可。sigaction結構體可以告訴作業系統程序接收到某個信号時就調用結構體裡的函數,這個函數叫做處理器,因為它将用來處理發送給它的信号,而且這個處理器必須接收信号參數,信号是一個整型值。**

定義一個處理器byebye():

void byebye(int sig){ //必須接收信号
  puts("Goodbye!!!");
  exit(1);
  }      

因為我們使用參數的形式接收信号,是以多個信号可以共用一個處理器,也可以單獨為每個信号定義一個處理器。處理器的代碼應該短而快,剛好能處理接收到的信号就OK。

如果中斷信号的處理器不調用exit(),程式不會結束。

用sigaction()函數來注冊sigaction

建立sigaction以後,需要用sigaction()函數來讓作業系統知道它的存在。sigaction()函數的形式如下:

sigaction(signal_no,&new_action,&old_action)

它的參數分三部分:

信号編号:signal_no這個整型值代表了你希望處理的信号,通常會傳遞SIGINT或SIGQUIT這樣的标準信号

新動作:想注冊新的sigaction(那個結構體執行個體)的位址

舊動作:如果想儲存被替換的信号處理器,可以再傳一個sigaction結構體指針給它,如果不想儲存舊動作,可以設定為NULL。

如果sigaction()函數失敗,會傳回-1,并設定errno變量。

下面我們給個例子:

int register_handler(int sig,void (*handler)(int)){
        struct sigaction action;
        action.sa_handler = handler;
        sigemptyset(&action.sa_mask);
        action.sa_flags = 0;
        return sigaction(sig,&action,NULL);
}      

隻要把想捕捉的信号和處理器函數名(實際上函數名就是一個指針)傳給 register_handler()函數,就可以通過 sigaction()函數,把想捕捉的信号與處理器函數關聯起來。

下面我們給出一個完整的執行個體test6.c:

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>

void byebye(int sig);
int register_handler(int sig,void (*handler)(int));
int main(){
        if(register_handler(SIGINT,byebye) == -1){

                fprintf(stderr,"Can't map the handler");
                exit(2);
        }


        printf("%s","please type somethine:");
        char c[120];
        fgets(c,120,stdin);
        printf("TOM:%s",c);
        return 0;
}

void byebye(int sig){
        puts("Goodbye!!!");
        exit(1);
}
int register_handler(int sig,void (*handler)(int)){
        struct sigaction action; //建立新動作
        action.sa_handler = handler; //想計算機在收到sig信号時,執行的函數(處理器)
        sigemptyset(&action.sa_mask);//用掩碼來過濾sigaction要處理的信号,通常會用一個空的掩碼
        action.sa_flags = 0;//附加标志位,設定為0即可
        return sigaction(sig,&action,NULL);
}      

編譯運作:

~/Desktop/MyC$ gcc test6.c -o test6
~/Desktop/MyC$ ./test6
please type somethine:|      

程式運作起來了,正在等待輸入,如果這時我們按下ctrl-C,作業系統就會自動向程序發送中斷信号(SIGINT),然後我們在register_handler()函數裡注冊的sigaction就會處理這個信号,sigaction中會指向byebye()函數指針,程式會調用這個函數,顯示消息并調用exit()函數。

please type somethine:^CGoodbye!!!      

看!我們成功捕捉到中斷信号并執行了我們自己寫的處理器函數了。

我們再來看個例子,說說Linux平台上的kill指令是如何殺死程序的,再把程式運作起來:

~/Desktop/MyC$ ./test6
please type somethine:
|      

程式運作起來了,正在待使用者輸入。

我們檢視一下程序資訊:

~$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
wong     28463  5363  0 12:19 pts/1    00:00:00 ./test6      

用kill指令殺掉程序:

~$ kill 28463      

就這樣test6的程序被kill指令殺死了。事實上,kill指令隻是向程序發送了一個信号,而kill指令預設會向程序發送SIGTERM信号,當然你可以用它發送其他信号,如SIGINT:

~$ kill -s SIGINT 28463      

有一個信号是程式無捕捉,又無法忽略的:SIGKILL信号,也就是說,即使程式中有一個錯誤導緻程序對任何信号都視而不見,還是能用kill -KILL結束程序:

~$ kill -KILL 28463      

讓程序向自己發送信号

哈哈哈!其實真的可以做到。可以用raise() 函數實作。通常會在自定義的信号處理函數中使用raise(),這樣程式就能在接收到低級别的信号時引發更進階别的信号,這叫信号更新。如在上述執行個體的byebye函數裡做個信号更新的操作:

void byebye(int sig){
        raise(SIGTERM);
        puts("Goodbye!!!");
        exit(1);
}      

修改完byebye處理器函數,編譯運作一下,然後按ctrl-C:

~/Desktop/MyC$ gcc test6.c -o test6
~/Desktop/MyC$ ./test6
please type somethine:^CTerminated      

從輸出的資訊來看,信号更新成功了。

上面提到的信号都是作業系統發送給程序的。其實有時程序也需要産生自己的信号,比如說鬧鐘信号SIGALRM。鬧鐘信号通常由程序的間隔定時器建立。間隔定時器就像一台鬧鐘:你可以定一個時間,其間程式就會去做其他事情。

當程序收到信号後就會停止一切工作來處理信号。程序收到鬧鐘信号後預設會結束程序。但這一般不是我們想要的。我們可以讓它收到這個鬧鐘信号後執行我們的代碼,方式和前面的一樣,隻是信号變成SIGALRM。

如果想還原信号的預設處理器,可以這樣做:

register_handler(SIGALRM,SIG_DFL);      

signal.h頭檔案裡有一個特殊的符号SIG_DFL,它代表以預設方式處理信号。

還可以用SIG_IGN讓程序忽略某個信号:

register_handler(SIGALRM,SIG_IGN);      

注意:程序在處理信号時會停止一切工作 ,也就是一次隻能做一件事。

在文章最後,我們把作業系統可以向程序發送的各種信号,大概列一下吧:

信号 描述
SIGINT 程序被中斷
SIGQUIT 有人要求停止程序,并把存儲器中的内容儲存到核心轉儲檔案
SIGFPE 浮點錯誤
SIGTRAP 調試人員詢問程序執行到了哪裡
SIGSEGV 程序企圖通路非法存儲器位址
SIGWINCH 終端視窗的大小發生改變
SIGTERM 有人要求核心終止程式