信號註冊
入門版函數 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);
https://jameshfisher.com/2017/01/10/c-signal-return-value/ ↩︎