信號


信號是指示事件發生的進程的通知。 信號也被稱為軟體中斷,不可預測知道它的發生,因此它也被稱為非同步事件。

信號可以用數位或名稱來指定,通常信號名稱以SIG開頭。 可用的信號可以用命令kill -l(l列出信號名稱)來檢查,如下所示 -
信號

無論何時發出信號(以程式設計方式或系統產生的信號),都會執行預設操作。 如果您不想執行預設操作但希望在接收信號時執行自己的操作。 可以處理信號,但不能處理所有的信號。 如果你想忽略信號,這是可能的嗎? 這是可以忽略信號。 忽略信號意味著既不執行預設操作也不處理信號。 可以忽略或處理幾乎所有的信號。 SIGSTOPSIGKILL不能被忽略或處理/捕獲的信號。

總之,對信號執行的操作如下 -

  • 預設操作
  • 處理信號
  • 忽略信號

正如所討論的,信號可以被處理,改變預設行為的執行。 信號處理可以通過系統呼叫,signal()sigaction()兩種方式完成。

#include <signal.h>

typedef void (*sighandler_t) (int);
sighandler_t signal(int signum, sighandler_t handler);

系統呼叫signal()將在符號中提到的信號產生時呼叫註冊的處理程式。 處理程式可以是SIG_IGN(忽略信號),SIG_DFL(將信號設定回預設機制)或使用者定義的信號處理程式或函式地址之一。

這個系統呼叫成功返回一個函式的地址,該函式接受一個整數引數並且沒有返回值。 這個呼叫在出錯的時候返回SIG_ERR

雖然signal()可以呼叫使用者註冊的各自的信號處理程式,但是不可能進行微調,例如掩蔽應該被阻塞的信號,修改信號的行為以及其他功能。 這可以使用sigaction()系統呼叫。

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact)

這個系統呼叫用於檢查或改變信號動作。 如果行為不為空,則從行為安裝信號的新行為。 如果oldact不為null,則以前的操作將儲存在oldact中。

sigaction結構包含以下欄位 -

欄位1 - 處理程式在sa_handlersa_sigaction中提到。

void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);

sa_handler的處理程式指定要根據signumSIG_DFL指示預設操作執行的操作,或者SIG_IGN忽略指向信號處理常式的信號或指標。
sa_sigaction的處理程式將信號編號指定為第一個引數,將指向siginfo_t結構體的指標指定為第二個引數,並指定使用者上下文(有關詳細資訊,請參閱getcontext()setcontext())作為第三個引數。

siginfo_t結構包含信號資訊,例如要傳遞的信號數量,信號值,進程ID,傳送進程的真實使用者ID等。

欄位2 - 要阻止的信號集。

int sa_mask;

這個變數指定了信號處理程式執行期間應該被阻塞的信號掩碼。

欄位3 - 特殊標誌。

int sa_flags;

該欄位指定一組修改信號行為的標誌。

欄位4 - 恢復處理程式。

void (*sa_restorer) (void);

這個系統呼叫在成功時返回0,在失敗的情況下為-1

讓我們考慮一些範例程式。

首先,從一個生成異常的範例程式開始。 在這個程式中,試圖執行除零運算,這會使系統產生一個異常。

/* signal_fpe.c */
#include<stdio.h>

int main() {
   int result;
   int v1, v2;
   v1 = 121;
   v2 = 0;
   result = v1/v2;
   printf("Result of Divide by Zero is %d\n", result);
   return 0;
}

編譯和執行上面範例程式碼,得到以下結果 -

Floating point exception (core dumped)

因此,當試圖執行一個算術運算時,系統已經產生了一個浮點異常,核心轉儲是信號的預設操作。

現在,修改程式碼以使用signal()系統呼叫來處理這個特定的信號。

/* signal_fpe_handler.c */
#include<stdio.h>
#include<signal.h>
#include<stdlib.h>

void handler_dividebyzero(int signum);

int main() {
   int result;
   int v1, v2;
   void (*sigHandlerReturn)(int);
   sigHandlerReturn = signal(SIGFPE, handler_dividebyzero);
   if (sigHandlerReturn == SIG_ERR) {
      perror("Signal Error: ");
      return 1;
   }
   v1 = 121;
   v2 = 0;
   result = v1/v2;
   printf("Result of Divide by Zero is %d\n", result);
   return 0;
}

void handler_dividebyzero(int signum) {
   if (signum == SIGFPE) {
      printf("Received SIGFPE, Divide by Zero Exception\n");
      exit (0);
   } 
   else
      printf("Received %d Signal\n", signum);
      return;
}

執行上面範例程式碼,得到以下結果 -

Received SIGFPE, Divide by Zero Exception

正如所討論的那樣,信號是由系統產生的(在執行某些操作(例如除以零等)時),或者使用者也可以以程式設計方式產生信號。 如果要以程式設計方式生成信號,請使用庫函式raise()

現在,另外一個程式來演示處理和忽略信號。

假設用raise()發出了一個信號,那麼會發生什麼? 發出信號後,當前進程的執行停止。 那麼停止的過程會發生什麼? 可以有兩種情況 - 首先,在需要時繼續執行。 其次,終止(用kill命令)這個進程。

要繼續執行已停止的進程,請將SIGCONT傳送到該特定進程。 也可以發出fg(前景)或bg(後台)命令來繼續執行。 在這裡,這些命令只會重新開始執行最後一個進程。 如果多個進程停止,則只有最後一個進程被恢復。 如果想恢復先前停止的過程,請使用fg/bg和作業編號恢復作業。

下面的程式用於使用raise()函式來提升信號SIGSTOP。 信號SIGSTOP也可以由使用者按下CTRL + Z(Ctrl + Z)鍵生成。 發出此信號後,程式將停止執行。 傳送信號(SIGCONT)繼續執行。

在下面的例子中,使用命令fg重新開始停止的進程。

/* signal_raising.c */
#include<stdio.h>
#include<signal.h>
#include<stdlib.h>

int main() {
   printf("Testing SIGSTOP\n");
   raise(SIGSTOP);
   return 0;
}

執行上面範例程式碼,得到以下結果 -

Testing SIGSTOP
[1]+ Stopped ./a.out
./a.out

現在,增強以前的程式,通過從另一個終端發出SIGCONT來繼續執行停止的進程。

/* signal_stop_continue.c */
#include<stdio.h>
#include<signal.h>
#include <sys/types.h>
#include <unistd.h>

void handler_sigtstp(int signum);

int main() {
   pid_t pid;
   printf("Testing SIGSTOP\n");
   pid = getpid();
   printf("Open Another Terminal and issue following command\n");
   printf("kill -SIGCONT %d or kill -CONT %d or kill -18 %d\n", pid, pid, pid);
   raise(SIGSTOP);
   printf("Received signal SIGCONT\n");
   return 0;
}

執行上面範例程式碼,得到以下結果 -

Testing SIGSTOP
Open Another Terminal and issue following command
kill -SIGCONT 30379 or kill -CONT 30379 or kill -18 30379
[1]+ Stopped ./a.out

Received signal SIGCONT
[1]+ Done ./a.out

在另一個終端執行 -

kill -SIGCONT 30379

到目前為止,我們已經看到了處理系統產生的信號的程式。 現在讓我們看看通過程式產生的信號(使用raise()函式或者通過kill命令)。 該程式產生信號SIGTSTP(終端停止),其預設動作是停止執行。 但是,由於現在正在處理信號,而不是預設的動作,它會來到定義的處理程式。 在這種情況下,只是列印訊息並退出。

/* signal_raising_handling.c */
#include<stdio.h>
#include<signal.h>
#include<stdlib.h>

void handler_sigtstp(int signum);

int main() {
   void (*sigHandlerReturn)(int);
   sigHandlerReturn = signal(SIGTSTP, handler_sigtstp);
   if (sigHandlerReturn == SIG_ERR) {
      perror("Signal Error: ");
      return 1;
   }
   printf("Testing SIGTSTP\n");
   raise(SIGTSTP);
   return 0;
}

void handler_sigtstp(int signum) {
   if (signum == SIGTSTP) {
      printf("Received SIGTSTP\n");
      exit(0);
   }
   else
      printf("Received %d Signal\n", signum);
      return;
}

執行上面範例程式碼,得到以下結果 -

Testing SIGTSTP
Received SIGTSTP

已經看到了執行預設操作或處理信號的情況。 現在是時候忽略這個信號了。 在這個範例程式中,通過SIG_IGN註冊信號SIGTSTP忽略,然後傳送信號SIGTSTP(終端停止)。 當信號SIGTSTP正在被生成,將被忽略。參考以下程式碼 -

/* signal_raising_ignoring.c */
#include<stdio.h>
#include<signal.h>
#include<stdlib.h>

void handler_sigtstp(int signum);

int main() {
   void (*sigHandlerReturn)(int);
   sigHandlerReturn = signal(SIGTSTP, SIG_IGN);
   if (sigHandlerReturn == SIG_ERR) {
      perror("Signal Error: ");
      return 1;
   }
   printf("Testing SIGTSTP\n");
   raise(SIGTSTP);
   printf("Signal SIGTSTP is ignored\n");
   return 0;
}

執行上面範例程式碼,得到以下結果 -

Testing SIGTSTP
Signal SIGTSTP is ignored

到目前為止,我們已經觀察到,一個信號處理程式用來處理一個信號。 那麼一個信號可以處理多個信號嗎? 答案是肯定的 讓我們通過一個程式來實現。

以下程式執行以下操作 -

第1步 - 註冊一個處理程式(handleSignals)來捕獲或處理信號SIGINT(CTRL + C)或SIGQUIT(CTRL + )

第2步 - 如果使用者產生信號SIGQUIT(或者通過kill命令或者用CTRL + \鍵盤控制),處理程式簡單地將訊息列印為返回。

第3步 - 如果使用者第一次生成信號SIGINT(通過kill命令或者使用CTRL + C的鍵盤控制),則修改信號從下次開始執行預設操作(使用SIG_DFL)。

第4步 - 如果使用者第二次生成信號SIGINT,則執行預設操作,即程式終止。

/* Filename: sigHandler.c */
#include<stdio.h>
#include<unistd.h>
#include<signal.h>

void handleSignals(int signum);

int main(void) {
   void (*sigHandlerInterrupt)(int);
   void (*sigHandlerQuit)(int);
   void (*sigHandlerReturn)(int);
   sigHandlerInterrupt = sigHandlerQuit = handleSignals;
   sigHandlerReturn = signal(SIGINT, sigHandlerInterrupt);
   if (sigHandlerReturn == SIG_ERR) {
      perror("signal error: ");
      return 1;
   }
   sigHandlerReturn = signal(SIGQUIT, sigHandlerQuit);

   if (sigHandlerReturn == SIG_ERR) {
      perror("signal error: ");
      return 1;
   }
   while (1) {
      printf("\nTo terminate this program, perform the following: \n");
      printf("1. Open another terminal\n");
      printf("2. Issue command: kill %d or issue CTRL+C 2 times (second time it terminates)\n", getpid());
      sleep(10);
   }
   return 0;
}

void handleSignals(int signum) {
   switch(signum) {
      case SIGINT:
      printf("\nYou pressed CTRL+C \n");
      printf("Now reverting SIGINT signal to default action\n");
      signal(SIGINT, SIG_DFL);
      break;
      case SIGQUIT:
      printf("\nYou pressed CTRL+\\ \n");
      break;
      default:
      printf("\nReceived signal number %d\n", signum);
      break;
   }
   return;
}

編譯和執行上面範例程式碼,得到以下結果 -

To terminate this program, perform the following:
1. Open another terminal
2. Issue command: kill 74 or issue CTRL+C 2 times (second time it terminates)
^C
You pressed CTRL+C
Now reverting SIGINT signal to default action

To terminate this program, perform the following:
1. Open another terminal
2. Issue command: kill 74 or issue CTRL+C 2 times (second time it terminates)
^\You pressed CTRL+\
To terminate this program, perform the following:
1. Open another terminal
2. Issue command: kill 120
Terminated

在另一個終端中執行 -

kill 71

第二種方法 -

To terminate this program, perform the following:
1. Open another terminal
2. Issue command: kill 71 or issue CTRL+C 2 times (second time it terminates)
^C
You pressed CTRL+C
Now reverting SIGINT signal to default action

To terminate this program, perform the following:
1. Open another terminal
2. Issue command: kill 71 or issue CTRL+C 2 times (second time it terminates)
^C

我們知道要處理一個信號,有兩個系統呼叫,即signal()sigaction()。 直到現在我們已經看到了signal()系統呼叫,現在是時候進行sigaction()系統呼叫了。 讓我們修改上面的程式來執行使用sigaction(),如下所示 -

/* Filename: sigHandlerSigAction.c */
#include<stdio.h>
#include<unistd.h>
#include<signal.h>

void handleSignals(int signum);

int main(void) {
   void (*sigHandlerReturn)(int);
   struct sigaction mysigaction;
   mysigaction.sa_handler = handleSignals;
   sigemptyset(&mysigaction.sa_mask);
   mysigaction.sa_flags = 0;
   sigaction(SIGINT, &mysigaction, NULL);

   if (mysigaction.sa_handler == SIG_ERR) {
      perror("signal error: ");
      return 1;
   }
   mysigaction.sa_handler = handleSignals;
   sigemptyset(&mysigaction.sa_mask);
   mysigaction.sa_flags = 0;
   sigaction(SIGQUIT, &mysigaction, NULL);

   if (mysigaction.sa_handler == SIG_ERR) {
      perror("signal error: ");
      return 1;
   }
   while (-1) {
      printf("\nTo terminate this program, perform either of the following: \n");
      printf("1. Open another terminal and issue command: kill %d\n", getpid());
      printf("2. Issue CTRL+C 2 times (second time it terminates)\n");
      sleep(10);
   }
   return 0;
}

void handleSignals(int signum) {
   switch(signum) {
      case SIGINT:
      printf("\nYou have entered CTRL+C \n");
      printf("Now reverting SIGINT signal to perform default action\n");
      signal(SIGINT, SIG_DFL);
      break;
      case SIGQUIT:
      printf("\nYou have entered CTRL+\\ \n");
      break;
      default:
      printf("\nReceived signal number %d\n", signum);
      break;
   }
   return;
}

讓我們編譯和執行上面的程式碼。 在執行過程中,按CTRL + C兩次,其餘的檢查/方式(如上)來嘗試執行這個程式。

執行上面範例程式碼,得到以下結果 -

To terminate this program, perform either of the following:
1. Open another terminal and issue command: kill 3199
2. Issue CTRL+C 2 times (second time it terminates)
^C
You have entered CTRL+C
Now reverting SIGINT signal to perform default action
To terminate this program, perform either of the following:
1. Open another terminal and issue command: kill 3199
2. Issue CTRL+C 2 times (second time it terminates)
^C