Linux 信號 signal

信號註冊

入門版函數 signal

使用需要包含 <signal.h> 這個頭文件。

signal(參數1,參數2);
參數1:我們要進行處理的信號。系統的信號我們可以再終端鍵入 kill -l 查看(64)。
參數2:我們處理的方式(系統默認 / 忽略 / 捕獲)。
signal(SIGINT, SIG_ING ); // ignore the signal
signal(SIGINT, SIG_DFL);  // use the default handler
signal(SIGINT, userfunc); // user defined handler 

signal函數的原型爲
void ( *signal( int sig, void (* handler)( int )))( int );
這個比較有意思,容易把人看懵。

void (*p)(int); // p 是一個函數指針,參數是 int 類型,無返回值
void (*p())(int); // p 是一個函數,p的返回值是一個函數指針,參數是 int 類型,無返回值
// 然後再來看signal的定義,有沒有清楚一些...
signal 函數的返回值是一個函數指針,該指針指向一個參數是 int 類型,無返回值的函數。
signal 函數本身的參數是 int 類型以及一個函數指針,void (* handler)(int)// 簡化的寫法如下
typedef void (*handler)(int);
handler signal(int, handler);
// signal 函數返回的是上一次處理該信號的函數指針,如果沒有就返回 NULL

signal 函數返回值1

signal 函數返回的是上一次處理該信號的函數指針,如果沒有就返回 NULL。
如果不關心返回值也可以不寫,好的編程習慣是加個判斷, if (signal(SIGINT, sigint_handler)) == SIG_ERR)

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

void catch1(int signo) {
  printf("catch1 received signal %d\n", signo);
}

void catch2(int signo) {
  printf("catch2 received signal %d\n", signo);
}

int main(int argc, char *argv[]) {
  sig_t prev_sigint_handler1 = signal(SIGINT, catch1);
  assert(prev_sigint_handler1 == NULL);
  
  sig_t prev_sighup_handler1 = signal(SIGHUP, catch2);
  assert(prev_sighup_handler1 == NULL);

  raise(SIGINT);  // calls catch1
  raise(SIGHUP);  // calls catch2

  // Now let's swap the handlers

  sig_t prev_sigint_handler2 = signal(SIGINT, catch2);
  assert(prev_sigint_handler2 == catch1);

  sig_t prev_sighup_handler2 = signal(SIGHUP, catch1);
  assert(prev_sighup_handler2 == catch2);

  raise(SIGINT);  // calls catch2
  raise(SIGHUP);  // calls catch1

  return 0;
}
% ./a.out
catch1 received signal 2
catch2 received signal 1
catch2 received signal 2
catch1 received signal 1

高級版函數 sigaction

// man sigaction
#include <signal.h>
/**
 *  註冊信號處理函數,成功返回0,失敗返回-1並置 errno
 *  參數 act 存儲待註冊的信號處理函數結構體
 *  oldact 非空的話,舊的信號處理函數會存儲到該結構體中
 */
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
 
struct sigaction {
    void     (*sa_handler)(int);
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t   sa_mask;
    int        sa_flags;
    void     (*sa_restorer)(void);
};
// 回調函數句柄 sa_handler、sa_sigaction 只能任選其一
該結構在註冊信號處理函數sigaction中使用
1. sa_handler 是一個參數爲信號值的處理函數
2. sa_sigaction 也是一個信號處理函數,不過它有三個參數,能夠獲取到處信號值以外更多
   信息,當 sa_flags 中包含 SA_SIGINFO 標誌位的時候需要用到該函數。
3. sa_mask 是信號處理函數執行期間的屏蔽信號集。就是說在信號處理函數執行期間,屏蔽某
   些信號。但是不是所有信號都能夠被屏蔽,SIGKILL 和 SIGSTOP 這兩個信號就無法屏
   蔽,因爲 OS 自身要能夠控制住進程。
4. sa_flags可以是下面這些值的集合:
   1. SA_NOCLDSTOP,
      這個標誌位只用於SIGCHLD信號。父進程可以檢測子進程三個事件,子進程終止、
      子進程停止、子進程恢復。SA_NOCLDSTOP標誌位用於控制後兩個事件。即一旦父進程
      爲SIGCHLD信號設置了這個標誌位,那麼子進程停止和子進程恢復這兩件事情,就無需
      向父進程發送 SIGCHLD 信號。
   2. SA_NOCLDWAIT
      這個標誌只用於 SIGCHLD 信號,它可控制子進程終止時候的行爲,如果父進程
      爲 SIGCHLD 設置了 SA_NOCLDWAIT 標誌位,那麼子進程終止退出時,就不會進入殭屍
      狀態,而是直接自行了斷。但是對 Linux 而言,子進程仍然會發送 SIGCHLD 信號,這
      點和上面的 SA_NOCLDSTOP 略有不同。
   3. SA_ONESHOT 和 SA_RESETHAND
      這兩個標誌位本質是一樣的,表示信號處理函數是一次性的,信號遞送出去以後,信號
      處理函數便恢復成默認值 SIG_DFL。
   4. SA_NODEFER 和 SA_NOMASK
      這兩個標誌位的作用是一樣的,信號處理函數執行期間,不阻塞當前信號。
   5. SA_RESTART
      這個標誌位表示,如果系統調用被信號中斷,則不返回錯誤,而是自動重啓系統調用。
   6. SA_SIGINFO
      這個標誌位表示信號發送者會提供額外的信息。這種情況下,信號處理函數應該爲
      三參數的函數。

siginfo_t 的內容如下

siginfo_t {
	int      si_signo;     /* Signal number */
	int      si_errno;     /* An errno value */
	int      si_code;      /* Signal code */
	int      si_trapno;    /* Trap number that caused
							 hardware-generated signal
							 (unused on most architectures) */
	pid_t    si_pid;       /* Sending process ID */
	uid_t    si_uid;       /* Real user ID of sending process */
	int      si_status;    /* Exit value or signal */
	clock_t  si_utime;     /* User time consumed */
	clock_t  si_stime;     /* System time consumed */
	sigval_t si_value;     /* Signal value */
	int      si_int;       /* POSIX.1b signal */
	void    *si_ptr;       /* POSIX.1b signal */
	int      si_overrun;   /* Timer overrun count;
							 POSIX.1b timers */
	int      si_timerid;   /* Timer ID; POSIX.1b timers */
	void    *si_addr;      /* Memory location which caused fault */
	long     si_band;      /* Band event (was int in
							 glibc 2.3.2 and earlier) */
	int      si_fd;        /* File descriptor */
	short    si_addr_lsb;  /* Least significant bit of address
							 (since Linux 2.6.32) */
	void    *si_lower;     /* Lower bound when address violation
							 occurred (since Linux 3.19) */
	void    *si_upper;     /* Upper bound when address violation
							 occurred (since Linux 3.19) */
	int      si_pkey;      /* Protection key on PTE that caused
							 fault (since Linux 4.6) */
	void    *si_call_addr; /* Address of system call instruction
							 (since Linux 3.5) */
	int      si_syscall;   /* Number of attempted system call
	unsigned int si_arch;  /* Architecture of attempted system call
							 (since Linux 3.5) */
}

常用信號

其中SIGSTOP以及SIGKILL 無法被捕獲和忽略。無法被捕獲指不能指定用戶的處理函數。

Signal Description
SIGABRT 由調用abort函數產生,進程非正常退出
SIGALRM 用alarm函數設置的timer超時或setitimer函數設置的interval timer超時
SIGBUS 某種特定的硬件異常,通常由內存訪問引起
SIGCANCEL 由Solaris Thread Library內部使用,通常不會使用
SIGCHLD 進程Terminate或Stop的時候,SIGCHLD會發送給它的父進程。缺省情況下該Signal會被忽略
SIGCONT 當被stop的進程恢復運行的時候,自動發送
SIGEMT 和實現相關的硬件異常
SIGFPE 數學相關的異常,如被0除,浮點溢出,等等
SIGFREEZE Solaris專用,Hiberate或者Suspended時候發送
SIGHUP 發送給具有Terminal的Controlling Process,當terminal被disconnect時候發送
SIGILL 非法指令異常
SIGINFO BSD signal。由Status Key產生,通常是CTRL+T。發送給所有Foreground Group的進程
SIGINT 由Interrupt Key產生,通常是CTRL+C或者DELETE。發送給所有ForeGround Group的進程
SIGIO 異步IO事件
SIGIOT 實現相關的硬件異常,一般對應SIGABRT
SIGKILL 無法處理和忽略。中止某個進程
SIGLWP 由Solaris Thread Libray內部使用
SIGPIPE 在reader中止之後寫Pipe的時候發送
SIGPOLL 當某個事件發送給Pollable Device的時候發送
SIGPROF Setitimer指定的Profiling Interval Timer所產生
SIGPWR 和系統相關。和UPS相關。
SIGQUIT 輸入Quit Key的時候(CTRL+\)發送給所有Foreground Group的進程
SIGSEGV 非法內存訪問
SIGSTKFLT Linux專用,數學協處理器的棧異常
SIGSTOP 中止進程。無法處理和忽略。
SIGSYS 非法系統調用
SIGTERM 請求中止進程,kill命令缺省發送
SIGTHAW Solaris專用,從Suspend恢復時候發送
SIGTRAP 實現相關的硬件異常。一般是調試異常
SIGTSTP Suspend Key,一般是Ctrl+Z。發送給所有Foreground Group的進程
SIGTTIN 當Background Group的進程嘗試讀取Terminal的時候發送
SIGTTOU 當Background Group的進程嘗試寫Terminal的時候發送
SIGURG 當out-of-band data接收的時候可能發送
SIGUSR1 用戶自定義signal 1
SIGUSR2 用戶自定義signal 2
SIGVTALRM setitimer函數設置的Virtual Interval Timer超時的時候
SIGWAITING Solaris Thread Library內部實現專用
SIGWINCH 當Terminal的窗口大小改變的時候,發送給Foreground Group的所有進程
SIGXCPU 當CPU時間限制超時的時候
SIGXFSZ 進程超過文件大小限制
SIGXRES Solaris專用,進程超過資源限制的時候發送

信號的處理

接收到信號後的處理動作有以下三種:

  • 忽略此信號
  • 執行信號的默認動作
  • 提供一個信號處理函數,要求內核處理該信號時切換到用戶執行這個處理函數,這種方式稱爲捕捉一個信號。

實際上我們還可以在接收信號前屏蔽它,叫做信號阻塞,這裏不詳述,感興趣的可以再查閱資料。

注意:忽略此信號是指接收到了信號後不做處理,阻塞的意思是信號不送達,但是還是有可能在我們更改信號的處理方式後被處理的,比如將忽略信號調整爲執行默認的動作後不再阻塞該信號。

信號 SIGCHLD 的產生條件

  • 子進程終止時
  • 子進程接收到 SIGSTOP 信號停止時
  • 子進程處在停止態,接受到 SIGCONT 後喚醒時

wait & waitpid

自看寶典

pid_t wait(int *status);
進程一旦調用了wait,就立即阻塞自己,由wait自動分析是否當前進程的某個子進程已經退出,如果讓它找到了這樣一個已經變成殭屍的子進程,wait就會收集這個子進程的信息,並把它徹底銷燬後返回;如果沒有找到這樣一個子進程,wait就會一直阻塞在這裏,直到有一個出現爲止。
pid_t waitpid(pid_t pid, int *status, int option);
wait(&status) == waitpid(-1, &status, 0);

These system calls are used to wait for state changes in a child of the calling process, and obtain information about the child whose state has changed. A state change is considered to be:

  • the child terminated;
  • the child was stopped by a signal;
  • or the child was resumed by a signal.

In the case of a terminated child, performing a wait allows the system to release the resources associated with the child; if a wait is not performed, then the terminated child remains in a “zombie” state.

pid 參數

  • pid == -1 等待任一子進程。於是在這一功能方面 waitpid 與 wait 等效。
  • pid > 0 等待其進程 ID 與 pid 相等的子進程。
  • pid == 0 等待其組 ID 等於調用進程的組 ID 的任一子進程。換句話說是與調用者進程同在一個組的進程。
  • pid < -1 等待其組 ID 等於 pid 的絕對值的任一子進程。

option 參數

By default, waitpid() waits only for terminated children, but this behavior is modifiable via the options argument, as described below. The value of options is an OR of zero or more of the following constants:

option description
WNOHANG 沒有子進程結束,立即返回
WUNTRACED 如果子進程由於被停止產生的SIGCHLD,waitpid則立即返回
WCONTINUED 如果子進程由於被SIGCONT喚醒而產生的SIGCHLD,waitpid則立即返回

返回值

如果成功返回等待子進程的 ID,失敗返回-1。

獲取 status

Marcro description
WIFEXITED(status) 子進程正常exit終止,返回真
WEXITSTATUS(status) 返回子進程正常退出值
WIFSIGNALED(status) 子進程被信號終止,返回真
WTERMSIG(status) 返回終止子進程的信號值
WIFSTOPPED(status) 子進程被信號停止,返回真
WSTOPSIG(status) 返回停止子進程的信號值
WIFCONTINUED(status) 子進程被信號SIGCONT恢復運行,返回真
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <unistd.h>

void _exit1(void) {
    printf("parent _exit1\n");
}

void _exit2(void) {
    printf("child _exit2\n");
}

void signalhandler(int signum) {
    if (signum == SIGIO) {
       printf("pid: %d catch SIGIO\n", getpid());
    } else if (signum == SIGUSR2) {
       printf("pid: %d catch SIGUSR2\n", getpid());
    } else {
       printf("pid: %d catch error\n", getpid());
    }
}

int main(int argc, char *argv[]) {
    pid_t pid, ret;
    int status;
    atexit(_exit1); // child inherit the funtion

    pid = fork();
    if (pid < 0) {
        perror("fork error");
        exit(EXIT_FAILURE);
    }

    if( pid == 0) {
        //signal(SIGUSR2, SIG_DFL); // exit abnomal with SIGUSR2
        signal(SIGUSR2, signalhandler); // child handle this signal and exit normally
        atexit(_exit2); // called first
        printf("This is the child process\n");
        //exit(100);  // atexit functions called
        //_exit(200); // no atexit functions called
        //abort();    // exit abnormal with SIGABORT
        //raise(SIGSTOP); // raise SIGSTOP to itself
        raise(SIGUSR2); // raise SIGUSER2 to itself
        return 0;
    }
    
    ret = waitpid(pid, &status, 0 | WUNTRACED); // wait for child termial or stopped
    if (ret < 0) {
        perror("wait error");
        exit(EXIT_FAILURE);
    }

    printf("parent waitpid: ret = %d pid = %d\n", ret, pid);

    if (WIFEXITED(status)) {
        printf("child exited normal exit status = %d\n", WEXITSTATUS(status));
    } else if (WIFSIGNALED(status)) {
        printf("child exited abnormal signal number = %d\n", WTERMSIG(status));
    } else if (WIFSTOPPED(status)) {
        printf("child stoped signal number = %d\n", WSTOPSIG(status));
    }

    return 0;
}

在這裏插入圖片描述

信號的發送

sigqueue

#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
union sigval {
   int   sival_int;
   void *sival_ptr;
 };

raise

raise(signum)

kill

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);

日常我們想結束一個進程會使用這樣的命令,kill -9 pid,實際上就是在向進程號爲 pid 的進程發送 SIGKILL

關於 kill 函數,還有一點需要額外說明, kill 函數傳入的 pid 可以是小於等於0的整數。
pid > 0:將發送給該 pid 的進程
pid == 0: 將會把信號發送給與發送進程屬於同一進程組的所有進程,並且發送進程具有權限向這些進程發送信號。
pid < 0:將信號發送給進程組ID 爲 pid 的絕對值的,並且發送進程具有權限向其發送信號的所有進程。
pid == -1:將該信號發送給發送進程的有權限向他發送信號的所有進程。

關於信號,還有更多的話題,比如,信號是否都能夠準確的送達到目標進程呢?
答案其實是不一定,因爲有可靠信號和不可靠信號的區分。

可靠信號與不可靠信號

對於信號來說,信號編號<=31的信號都是不可靠信號,之後的信號爲可靠信號,系統會根據有信號隊列,將信號在遞達之前進行阻塞。信號的阻塞和未決是通過信號的狀態字來管理的,狀態字按位管理。每個信號都有獨立的阻塞字,規定了當前要阻塞到達該進程的信號集。

  • 信號阻塞狀態字(block),1代表阻塞、0代表不阻塞;
  • 信號未決狀態字(pending),1代表未決,0代表信號可以抵達了。

阻塞狀態字用戶可讀寫,未決狀態字用戶只讀,它由內核來設置,表示信號遞達狀態。

block 設置

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how 變量決定了是如何操作該狀態字。

  • SIG_BLOCK:set 包含了我們希望添加到當前信號阻塞字的信號,相當於mask=mask|set
  • SIG_UNBLOCK:set 包含了我們希望從當前信號阻塞字中解除阻塞的信號,相當於mask=mask&~set
  • SIG_SETMASK:設置當前信號阻塞字爲set所指的值,相當於mask=set

上面的函數通過信號集的數據結構來進行信號阻塞的管理。
而信號集本身可以通過以下的函數進行配置。

#include <signal.h>
int sigemptyset(sigset_t *set);           // 初始化 set 中傳入的信號集,清空其中所有信號
int sigfillset(sigset_t *set);            // 把信號集填1,讓 set 包含所有的信號
int sigaddset(sigset_t *set, int signum); // 信號集對應位置 1
int sigdelset(sigset_t *set, int signum); // 信號集對應位清 0
int sigismember(const sigset_t *set, int signum); // 判斷 signal 是否在信號集

常用的步驟如下:

  • 分配內存空間 sigset bset;
  • 清空信號集,sigempty(&bset);
  • 添加要處理的信號 sigaddset(&bset, SIGINT);
  • 繼續添加其他的信號…
  • 設置相關的處理方案,阻塞與否。sigprocmask(SIG_UNBLOCK, &bset, NULL);

  1. https://jameshfisher.com/2017/01/10/c-signal-return-value/ ↩︎

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