淺談Linux中的信號處理機制(一)


     有好些日子沒有寫博客了,自己想想還是不要荒廢了時間,寫點兒東西記錄自己的成長還是百利無一害的。今天是9月17號,暑假在某家游戲公司實習了一段時間,做的事情是在Windows上用c++寫一些游戲英雄技能的邏輯實現。雖然時間不算長,但是也算學了一點東西,對團隊項目開發流程也有了一個直觀的感受,項目里c++11新特性也有用到不少,特別是lambda表達式,STL的一些容器和算法也終於有了可以實踐的地方。由於自己比較喜歡Linux C,也就沒有做留下的打算,現在回到了學校,好好復習一段時間,准備一下校招吧。如果有朋友有工作機會的話,不妨可以推薦一下我O(∩_∩)O,我的EMAIL:baixiangcpp@gamil.com。貌似有點扯遠了,好久不寫東西了。

信號的基本概念

  談起Linux編程signal是非常重要的一塊內容。關於signal,Linux 的 manual有很詳細的介紹。具體:man 7 signal .我就不浪費篇幅貼出來了。

  信號被認為是一種軟件中斷(區別於硬件中斷)。信號機制提供了一種在單進程/線程 下處理異步事件的方法。具體過程是當進程運行到某處,接受到一個信號,保留“現場”,響應信號(注意這里的響應是一種宏觀意義上的響應,對信號的忽略(SIG_IGN)也被以為是一種響應,后面會詳細談到信號響應的方式。),在返回到剛剛保存的地方繼續運行。我制作了一張GIF或許可以清晰的體現這樣的處理方式:

      產生信號的條件有很多,某些組合鍵(CTRL+C、CTRL+\,CTRL+Z等),kill命令,kill系統調用以及由內核產生的某些信號(如內核檢測到段錯誤、管道破裂等)。值得注意的是當我們發送信號時受到權限的限制,發送一個信號到另一個沒有權限的進程是不合法的(關於權限的規則會在之后的博客總結)。信號的種類非常多,都以SIG+名字的形式命名的宏,通常都有實際意義和用法具體可查閱manual。有些常見的信號是需要熟記的如SIGINT,SIGCHLD,SIGIO等等。在編寫程序的時候,我們最好用信號的宏的形式,這樣可讀性更好。那么如何“響應”信號呢?

信號處理的接口之一 signal()

      對於大部分的信號,Linux系統都有默認的處理方式。而大部分默認的處理方式是終止程序並轉儲core文件。要處理信號,Linux系統處理信號的接口有兩個sigaction(),signal(),較簡單的是signal()函數,其形式如下:

 typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

      siganl()函數有兩個參數其中有一個int的參數便是要處理的信號,諸如SIGINT的宏。另一個參數類型為sighandler_t的函數指針,handler指針對應的函數我們稱之為:信號處理函數(signal-handler function)。可見signal()的第二個參數是一個信號處理函數,返回值也是一個信號處理函數,失敗返回宏SIG_ERR(SIGKILL和SIGSTOP的默認行為分別是殺死和停止一個進程,任何試圖改變這兩個信號的處理方式的行為都將返回錯誤)。這樣經典形式的函數在Linux上我們經常會經常碰到。signal()函數的作用就是建立一個signum信號的處理函數(establish a signal handler function)。通俗一點來說就是當signum信號到來時,進程會保存當前的進程棧,轉去執行siganl()中指定的handler函數。之前提到過,信號的響應方式有多種,因此handler不僅可以是一個函數指針也可以是ISO C為我們定義的宏:SIG_IGN,SIG_DEL,和他們的名字一樣SIG_IGN是忽略這個信號,SIG_DEL是保持這個信號的默認處理方式(默認處理方式也可以可以是SIG_IGN ,比較繞,但是合理)。前文提到的三個宏定義分別如下(/usr/include/bits/signum.h):

#define SIG_ERR ((__sighandler_t) -1) 
#define SIG_DFL ((__sighandler_t) 0)
#define SIG_IGN ((__sighandler_t) 1)

      下面我寫一個小的DEMO演示一下如何寫一個信號處理函數:

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

void sigdemo(int sig)
{
    printf("Receive a signal:%s\n",strsignal(sig));
}

int main()
{
    if(signal(SIGINT,sigdemo) == SIG_ERR)
  {
    perror("signal()");
    return ;
  }
    printf("Main started.\n");
    pause();//wait a signal.
}

  

可以看到,我在main函數中並沒有主動調用sigdemo函數,可是運行程序后,當我們在中斷按下CTRL+C時(發送SIGINT信號,宏對應的值是2),出現了這樣的結果:

[baixiang ~$]a.out
 Main started.
 ^CReceive a signal:2

可見sigdemo函數得到了執行,其參數sig便是接受到的信號的值。要將信號的值,轉換為其意義string.h中提供了一個函數char* strsignal(int sig), 基本上看到該函數原型就知道這個函數怎么用了,在此我就不再浪費篇幅贅述了。

發送信號

     上文我們通過使用CTRL+C組合鍵發送信號SIGINT給當前的進程。但是這種方法只能發送少部分信號且並不適用所有的進程比如后台進程和守護進程。守護進程不必說,連終端都沒有。交互shell (interactive shell)在啟動一個后台進程的時候,會自動把中斷和退出信號設置為忽略,關於這點我在網上看到一篇不錯的博客:http://hongjiang.info/shell-script-background-process-ignore-sigint/ 。這樣的情況下就無法使用快捷鍵的方式了。這里我介紹幾種其他的發送信號的方式。

     首先是shell命令kill其用法如下:

kill [-s signal|-p] [-q sigval] [-a] [--] pid...

     -s  signal  signal可以是諸如SIGINT,SIGQUIT之類的宏,亦可以是1,2,3...這樣的值,可以隨意使用,你開心就好。

     -q queue  sigval是值,可以伴隨信號傳遞,但是這里只可以是一個integer,在進程中可以使用sigaction()接收到這個值,與之對應的是另一個函數sigqueue()。這里先不詳細介紹,下文會談到。

     pid就是目標進程的進程id,可以是一個或者多個。但是發送信號時,要確保你所使用的用戶是具有發送信號到目標進程的權限的。

     kill的選項遠不止這些,但是通常這些已經夠用了。如有興趣請自行 “man 1 kill”查看。

     和shell命令kill有一個同名的系統調用kill(),其原型是這樣的:

int kill(pid_t pid, int sig);

     相信看了上邊的shell指令,這個函數的用法就一目了然了吧。pid是目標進程的pid,sig是要發送的信號。和其他函數一樣它也是成功返回0,失敗-1。然而真的這么簡單嗎?事實上不是。pid這個參數在這里大有學問。它的取值不僅僅可以是進程id,它甚至可以是負的。如果你對linux下編程熟悉的話,這樣的用法肯定接觸過,獲取消息隊列時使用的msgrcv()函數,其中的msgtype參數也具有類似的用法。當然扯遠了。

    pid>0 此時正式最普通的一種情況,pid是要目標進程的pid。

    pid=0  那么kill()會將信號發送給調用進程同組的所有進程,也包括他自己。

    pid=-1 那么信號將被發送至所有它具有權限發送信號的每一個進程(init進程和調用進程除外)。

    pid<-1 信號會發送sig信號到組id等於該pid絕對值的進程組中的每一個進程

如果pid在以上四種情況之外,無法匹配到目標進程,那么就會返回-1,errno被設置為ESRCH。當沒有權限發送時kill()也將失敗返回-1,errno會被設置為EPERM。關於linux上權限是如何作用的細節,我爭取再后面的博客總結一下。

     與kill()類似的還有一個函數killpg(),用法簡單多了,也不浪費篇幅了,查看manual就能搞定。

     最后一個發送信號的函數是raise(),它只接受一個參數signal,然后把該信號傳遞給調用進程:

int raise(int sig);//成功返回0,失敗返回-1

    由於這個函數不需要引用進程ID,它是被納入C99標准的函數。

    除了這幾種產生信號的shell命令和函數之外還有一些情況下可以產生信號,比如alarm(),settimer()之類的一些與時間相關的函數,以及一些常見的軟硬件錯誤都會產生信號。詳細談這些貌似就有點淡化主題了,扯遠了。

不可靠信號與可靠信號的語義

      信號的可靠與不可靠主要體現在兩個方面:

  • 對於不可靠信號,進程每次處理信號后,都會將信號的處理方式設置為默認動作。而對於可靠信號,它的處理函數執行以后,對該信號的處理方式不會發生變化。
  • 信號可能會丟失。

     由於Linux信號機制基本上從早期的UNIX系統上的信號機制移植過來的,所以Linux仍舊支持這些早期的不可靠信號。但是Linux也對不可靠信號做了(上面兩點區別的第一小點)改進,即不可靠信號處理方式,不會在處理函數執行后變成默認方式。所以,在Linux上對於不可靠信號與可靠信號的區別就在於是否支持排隊。

     關於信號是否會丟失,我們看這樣兩段代碼,首先是rcv.c:

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

void fun(int sig)
{
    printf("Recvive a signal:%s\n",strsignal(sig));
}

int main()
{
    if(signal(SIGINT,fun) == SIG_ERR)
        perror("signal");
    printf("%d\n",getpid());
    while(1)
       pause();
}

     這段程序先安裝SIGINT的信號處理函數fun,fun函數只是打印信號信息。之后打印出進程id同時死循環等待信號。

另一段程序是send.c:

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

int main(int argc,char** argv)
{
    pid_t pid = (pid_t)atol(argv[1]);
    printf("%d\n",pid);
    int i = 0;
    for(;i<500;++i)
        kill(pid,SIGINT);
}

  

send程序從終端接收一個參數即目標進程的PID,然后向其發送500次SIGINT信號。下面我們分別把rcv.c,send.c編譯成rcv和send。首先運行rcv,打印出了進程pid 20273然后,我們在開一個終端運行send 20273,觀察到這樣的結果:

 

send明明發送了500個SIGINT信號,而rcv中只接受處理了13個SIGINT信號,這是怎么回事兒呢?明天再接着寫下去,今天太晚了,戰斗力不足有點犯困了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM