天天看點

Linux 程序間通信(三)信号通信

1 信号概述

     信号是在軟體層次上對中斷機制的一種模拟。 在原理上, 一個程序收到一個信号與處理器收到一個中斷請求可以說是一樣的。 信号是異步的, 一個程序不必通過任何操作來等待信号的到達, 事實上, 程序也不知道信号到底什麼時候到達。 信号可以直接進行使用者空間程序和核心程序之間的互動, 核心程序也可以利用它來通知使用者空間程序發生了哪些系統事件。

它可以在任何時候發給某一程序, 而無須知道該程序的狀态。 如果該程序目前并未處于執行态, 則該信号就由核心儲存起來, 直到該程序恢複執行再傳遞給它為止; 如果一個信号被程序設定為阻塞, 則該信号的傳遞被延遲, 直到其阻塞被取消時才被傳遞給程序。

      信号是程序間通信機制中唯一的異步通信機制, 可以看做是異步通知, 通知接收信号的程序有哪些事情發生了。 信号機制經過 Posix 實時擴充後, 功能更加強大, 除了基本通知功能外, 還可以傳遞附加資訊。

信号事件的發生有兩個來源:

硬體來源:如我們按下了鍵盤上的按鈕或者出現其他硬體故障;

軟體來源:最常用發送信号的系統函數有 kill()、raise()、alarm()、setitimer()和 sigqueue()等, 軟體來源還包括一些非法運算等操作。

程序可以通過 3 種方式來響應一個信号

1. 忽略信号

忽略信号即對信号不做任何處理, 其中, 有兩個信号不能忽略: SIGKILL 和 SIGSTOP。

2. 捕捉信号

定義信号處理函數, 當信号發生時, 執行相應的處理函數。

3. 執行預設操作

Linux 對每種信号都規定了預設操作,

如表所示。

Linux 程式間通信(三)信号通信

信号名                    信号id    含義

(1)SIGINT                    2       Ctrl+C時OS送給前台程序組中每個程序

(2)SIGABRT                6         調用abort函數,程序異常終止

(3)SIGPOLL SIGIO     8          訓示一個異步IO事件,在進階IO中提及

(4)SIGKILL                  9          殺死程序的終極辦法

(5)SIGSEGV              11        無效存儲通路時OS發出該信号

(6)SIGPIPE               13        涉及管道和socket

(7)SIGALARM          14         涉及alarm函數的實作

(8)SIGTERM            15          kill指令發送的OS預設終止信号

(9)SIGCHLD             17        子程序終止或停止時OS向其父程序發此信号

(10)

SIGUSR1                10            使用者自定義信号,作用和意義由應用自己定義

SIGUSR2                12

       一個完整的信号生命周期可以分為 3 個重要階段,這 3 個階段由 4 個重要事件來刻畫的:

信号産生、 信号在程序中注冊、 信号在程序中登出、 執行信号處理函數。

     這裡信号的産生、注冊、 登出等是指信号的内部實作機制, 而不是信号的函數實作。 是以, 信号注冊與否與本節後面講到的發送信号函數(如 kill()等) 及信号安裝函數(如 signal()等) 無關, 隻與信号值有關。相鄰兩個事件的時間間隔構成信号生命周期的一個階段。要注意這裡的信号處理有多種方式, 一般是由核心完成的, 當然也可以由使用者程序來完成, 故在此沒有明确指出。

信号的處理包括信号的發送、 捕獲及信号的處理, 它們有各自相對應的常見函數。

 發送信号的函數: kill()、 raise()。

 捕捉信号的函數: alarm()、 pause()。

 處理信号的函數: signal()、 sigaction()。

2 信号發送與捕捉

2.1 信号發送: kill()和 raise()

kill()函數同讀者熟知的 kill 系統指令一樣, 可以發送信号給程序或程序組(實際上, kill系統指令隻是 kill()函數的一個使用者接口)。 這裡需要注意的是, 它不僅可以中止程序(實際上發出 SIGKILL 信号), 也可以向程序發送其他信号。

與 kill()函數不同的是, raise()函數隻允許程序向自身發送信号。

 kill()函數的文法要點

Linux 程式間通信(三)信号通信

raise()函數的文法要點。  隻能程序向自身發送信号

Linux 程式間通信(三)信号通信

      下面的示例首先使用 fork()建立了一個子程序, 接着為了保證子程序不在父程序調用kill()之前退出, 在子程序中使用 raise()函數向自身發送 SIGSTOP 信号, 使子程序暫停。 接下來在父程序中調用 kill()向子程序發送信号, 在該示例中使用的是 SIGKILL, 讀者可以使用其他信号進行練習。

/* kill_raise.c */
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
  pid_t pid;
  int ret;
  /* 建立一個子程序 */
  if ((pid = fork()) < 0)
  {
    printf("Fork error\n");
    exit(1);
  } 
  if (pid == 0)
  {
    /* 在子程序中使用 raise()函數發出 SIGSTOP 信号, 使子程序暫停 */
                sleep(1);
    printf("Child(pid : %d) is waiting for any signal\n", getpid());
    raise(SIGSTOP);
    exit(0);
  }
  else
  {
    /* 在父程序中收集子程序發出的信号, 并調用 kill()函數進行相應的操作 */
    if ((waitpid(pid, NULL, WNOHANG)) == 0)
    {
      if ((ret = kill(pid, SIGKILL)) == 0)
      {
        printf("Parent kill %d\n",pid);
      }
    } 
    waitpid(pid, NULL, 0);
    exit(0);
  }
}      

程式運作結果如下:

$ ./kill_raise

Child(pid : 4877) is waiting for any signal

Parent kill 4877

2. 信号捕捉: alarm()、 pause()

     alarm()也稱為鬧鐘函數, 它可以在程序中設定一個定時器, 當定時器指定的時間到時,它就向程序發送 SIGALARM 信号。 要注意的是,一個程序隻能有一個鬧鐘時核心隻為每個程序配置一個時鐘,一個鬧鐘沒結束,又定義一個鬧鐘,是不會定義成功,且會傳回上次定義後剩下的時間,如果在調用 alarm()之前已設定過鬧鐘時間,且簽約個已經結束, 則任何以前的鬧鐘時間都被新值所代替。

      pause()函數用于将調用程序挂起直至捕捉到信号為止。 這個函數很常用, 通常可以用于判斷信号是否已到。

alarm()函數的文法要點。

alarm發出的信号預設是結束程序SIGALARM

Linux 程式間通信(三)信号通信

pause()函數的文法要點。

以下執行個體實際上已完成了一個簡單的 sleep()函數的功能, 由于 SIGALARM 預設的系統動作為終止該程序, 是以程式在列印資訊前就會被結束了, 代碼如下:

/* alarm_pause.c */
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
  /* 調用 alarm 定時器函數 */
  int ret = alarm(5);
  pause();
  printf("I have been waken up.\n",ret); /* 此語句不會被執行 */
}      

結果:

$./alarm_pause

Alarm clock

3 信号的處理

         信号處理的方法主要有兩種, 一種是使用 signal()函數, 另一種是使用信号集函數組。

下面分别介紹這兩種處理方式。

3.1 使用 signal()函數

       使用 signal()函數處理時, 隻需指出要處理的信号和處理函數即可。 它主要用于前 32 種非實時信号的處理, 不支援信号傳遞資訊, 但是由于使用簡單、 易于了解, 是以也受到很多程式員的歡迎。 Linux 還支援一個更健壯更新的信号處理函數 sigaction(), 推薦使用該函數。

signal()函數的文法。

Linux 程式間通信(三)信号通信

      這裡需要對該函數原型進行說明。 這個函數原型有點複雜: 首先該函數原型整體指向一個無傳回值并且帶一個整型參數的函數指針, 也就是信号的原始配置函數; 接着該原型又帶有兩個參數, 其中第 2 個參數可以是使用者自定義的信号處理函數的函數指針。

 sigaction()函數的文法要點。

Linux 程式間通信(三)信号通信

    這裡要說明的是 sigaction()函數中第 2 和第 3 個參數用到的 sigaction 結構, 這是一個看似非常複雜的結構, 希望讀者能夠慢慢閱讀此段内容。

 sigaction 結構體的定義, 代碼如下:

struct sigaction {
    void    (*sa_handler)(int);    /* addr of signal handler, or SIG_IGN, or SIG_DFL */
    sigset_t    sa_mask;           /* additional signals to block */
    int    sa_flags;               /* signal options */

    /* alternate handler */
    void    (*sa_sigaction)(int, siginfo_t *, void *);
};      

       sa_handler 是一個函數指針, 指定信号處理函數, 這裡除可以是使用者自定義的處理函數外, 還可以為 SIG_DFL(采用預設的處理方式) 或 SIG_IGN(忽略信号)。 它的處理函數隻有一個參數, 即信号值。

      sa_mask 是一個信号集, 它可以指定在信号處理程式執行過程中哪些信号應當被屏蔽,在調用信号捕獲函數前, 該信号集要加入到信号的信号屏蔽字中。

      sa_flags 中包含了許多标志位, 是對信号進行處理的各個選擇項。 它的常見可選值如表

Linux 程式間通信(三)信号通信

第1個執行個體表明了如何使用 signal()函數捕捉相應信号, 并做出給定的處理。這裡, my_func就是信号處理的函數指針, 讀者還可以将其改為 SIG_IGN 或 SIG_DFL 檢視運作結果。

第 2個執行個體是用 sigaction()函數實作同樣的功能。

以下是使用 signal()函數的示例:

/* signal.c */
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
/* 自定義信号處理函數 */
void my_func(int sign_no)
{
  if (sign_no == SIGINT)
  {
    printf("I have get SIGINT\n");
  }
  else if (sign_no == SIGQUIT)
  {
    printf("I have get SIGQUIT\n");
  }
  } 
int main()
{
  printf("Waiting for signal SIGINT or SIGQUIT...\n");
  /* 發出相應的信号, 并跳轉到信号處理函數處 */
  signal(SIGINT, my_func);
  signal(SIGQUIT, my_func);
  pause();
  exit(0);
}      

$ ./signal

Waiting for signal SIGINT or SIGQUIT...

I have get SIGINT                                          //(按 Ctrl+c 組合鍵)

$ ./signal

Waiting for signal SIGINT or SIGQUIT...

I have get SIGQUIT                                        //(按 Ctrl+\ 組合鍵)

以下是用 sigaction()函數實作同樣的功能,

/* sigaction.c */
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
/* 自定義信号處理函數 */
void my_func(int sign_no)
{
  if (sign_no == SIGINT)
  {
    printf("I have get SIGINT\n");
  }
  else if (sign_no == SIGQUIT)
  {
    printf("I have get SIGQUIT\n");
  }
  } 
int main()
{
  struct sigaction action;
  printf("Waiting for signal SIGINT or SIGQUIT...\n");
  /* sigaction 結構初始化 */
  action.sa_handler = my_func;
  sigemptyset(&action.sa_mask);
  action.sa_flags = 0;
  /* 發出相應的信号, 并跳轉到信号處理函數處 */
  sigaction(SIGINT, &action, 0);
  sigaction(SIGQUIT, &action, 0);
  pause();
  exit(0);
}      

3.2 信号集函數組

     使用信号集函數組處理信号時涉及一系列的函數, 這些函數按照調用的先後次序可分為以下幾大功能子產品: 

建立信号集、 注冊信号處理函數及檢測信号。

1.建立信号集主要用于處理使用者感興趣的一些信号, 其函數包括以下幾個。

 sigemptyset(): 将信号集初始化為空。

 sigfillset(): 将信号集初始化為包含所有已定義的信号集。

 sigaddset(): 将指定信号加入到信号集中。

 sigdelset(): 将指定信号從信号集中删除。

 sigismember(): 查詢指定信号是否在信号集中。

2.注冊信号處理函數主要用于決定程序如何處理信号。 這裡要注意的是, 信号集裡的信号并不是真正可以處理的信号, 隻有當信号的狀态處于非阻塞狀态時才會真正起作用。 是以,首先使用 sigprocmask()函數檢測并更改信号屏蔽字(信号屏蔽字是用來指定目前被阻塞的一組信号, 它們不會被程序接收), 然後使用 sigaction()函數來定義程序接收到特定信号後的行為。檢測信号是信号處理的後續步驟, 因為被阻塞的信号不會傳遞給程序, 是以這些信号就處于“未處理” 狀态(也就是程序不清楚它的存在)。 sigpending()函數允許程序檢測“未處理” 信号, 并進一步決定對它們做何處理。

首先介紹建立信号集的函數格式, 表列舉了這一組函數的文法要點。

Linux 程式間通信(三)信号通信
Linux 程式間通信(三)信号通信

表 4.15 列舉了 sigprocmask()函數的文法要點。

Linux 程式間通信(三)信号通信

此處, 若 set 是一個非空指針, 則參數 how 表示函數的操作方式; 若 how 為空, 則表示忽略此操作。

表 4.16 列舉了 sigpending()函數的文法要點。

Linux 程式間通信(三)信号通信

總之, 在處理信号時, 一般遵循如圖 4.6 所示的操作流程。

Linux 程式間通信(三)信号通信

4. 信号處理執行個體

      該執行個體首先把 SIGQUIT、 SIGINT 兩個信号加入信号集, 然後将該信号集設為阻塞狀态,并進入使用者輸入狀态。 使用者隻需按任意鍵, 就可以立刻将信号集設定為非阻塞狀态, 再對這兩個信号分别操作, 其中 SIGQUIT 執行預設操作, 而 SIGINT 執行使用者自定義函數的操作。

源代碼如下:

/* sigset.c */
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
/* 自定義的信号處理函數 */
void my_func(int signum)
{
  printf("If you want to quit,please try SIGQUIT\n");
}
int main()
{
  sigset_t set,pendset;
  struct sigaction action1,action2;
  /* 初始化信号集為空 */
  if (sigemptyset(&set) < 0)
  {
    perror("sigemptyset");
    exit(1);
  } 
  /* 将相應的信号加入信号集 */
  if (sigaddset(&set, SIGQUIT) < 0)
  {
    perror("sigaddset");
    exit(1);
  } 
  if (sigaddset(&set, SIGINT) < 0)
  {
    perror("sigaddset");
    exit(1);
  } 
  if (sigismember(&set, SIGINT))
  {
    sigemptyset(&action1.sa_mask);
    action1.sa_handler = my_func;
    action1.sa_flags = 0;
    sigaction(SIGINT, &action1, NULL);
  } 
  if (sigismember(&set, SIGQUIT))
  {
    sigemptyset(&action2.sa_mask);
    action2.sa_handler = SIG_DFL;
    action2.sa_flags = 0;
    sigaction(SIGQUIT, &action2,NULL);
  } 
  /* 設定信号集屏蔽字, 此時 set 中的信号不會被傳遞給程序, 暫時進入待處理狀态 */
  if (sigprocmask(SIG_BLOCK, &set, NULL) < 0)
  {
    perror("sigprocmask");
    exit(1);
  }
  else
  {
    printf("Signal set was blocked, Press any key!");
    getchar();
  } 
  /* 在信号屏蔽字中删除 set 中的信号 */
  if (sigprocmask(SIG_UNBLOCK, &set, NULL) < 0)
  {
    perror("sigprocmask");
    exit(1);
  }
  else
  {
    printf("Signal set is in unblock state\n");
  } 
  while(1);
  exit(0);
}      

     該程式的運作結果如下, 可以看見, 在信号處于阻塞狀态時, 所發出的信号對程序不起作用, 并且該信号進入待處理狀态。 讀者按任意鍵, 并且信号脫離了阻塞狀态後, 使用者發出的信号才能正常運作。 這裡 SIGINT 已按照使用者自定義的函數運作, 請讀者注意阻塞狀态下SIGINT 的處理和非阻塞狀态下 SIGINT 的處理有何不同。

$ ./sigset

Signal set was blocked, Press any key!              /* 此時按任何鍵可以解除阻塞屏蔽字 */

If you want to quit,please try SIGQUIT                    /* 阻塞狀态下 SIGINT 的處理 */

Signal set is in unblock state                              /* 從信号屏蔽字中删除 set 中的信号 */

If you want to quit,please try SIGQUIT               /* 非阻塞狀态下 SIGINT 的處理 */

If you want to quit,please try SIGQUIT

Quit                                                                     /* 非阻塞狀态下 SIGQUIT 的處理 */