訊號是系統響應某些條件而產生的一個事件,接收到該信的程式做出相應的處理。通常信是由錯誤產生的,如段錯誤(SIGSEGV
)。 但信還可以作為程式間通訊的一種方式,由一個程式傳送給另一個程式。
訊號定義在 signal.h
檔案中,以 SIG
作為開頭,可用 kill -l
命令檢視,詳細資訊參見 man 7 signal。
訊號處理
訊號可以通過 signal
和 sigaction
函式來註冊處理, signal
函式是 struct sigaction
中 sa_handler
的一種便捷實現。
signal
函式
原型:
void (*signal(int sig, void (*func)(int)))(int);
其中 sig
是需要捕獲的 signal number
, 後一個是捕獲到訊號後的處理函式指標,所以處理函式的原型必須是 void func(int)
,簡單的程式碼示例如下:
#include <stdio.h>
#include <string.h>
#include <signal.h>
static void
handler(int sig)
{
printf("Recieved signal: %d
", sig);
}
int
main(int argc, char *argv[])
{
signal(SIGINT, handler);
printf("Caught SIGINT, input `quit` to exit...
");
// wait signal caught
char buf[1024] = {0};
while (1) {
printf("Please input: ");
scanf("%s", buf);
if (strcmp(buf, "quit") == 0) {
break;
}
}
printf("Exit...
");
return 0;
}
另外 api
中也提供了下面 2 個特殊的 handler
:
-
SIG_IGN
忽略此訊號
-
SIG_DFL
恢復此訊號的預設行為
sigaction
函式
原型:
int sigaction(int sig, const struct sigaction *restrict act,
struct sigaction *restrict oact);
其中 sig
為 signal number
, act
指定訊號的處理行為, oact
如果不為 NULL
則返回訊號之前的處理行為。
struct sigaction
的主要成員如下:
型別 | 名稱 | 描述 |
---|---|---|
void(*) (int) | sa_handler | 處理函式指標,同 signal 函式中的 func 引數 |
sigset_t | sa_mask | 訊號遮蔽字,是指當前被阻塞的一組訊號,不能被當前程式收到 |
int | sa_flags | 處理行為修改器,指明哪種處理函式生效,詳見下文 |
void(*) (int, siginfo_t *, void *) | sa_sigaction | 處理函式指標,僅 sa_flags == SA_SIGINFO 時有效 |
其中 sa_flags
主要可以設定為以下值:
- SA_NOCLDSTOP
子程式停止時不產生
SIGCHLD
訊號 - SA_RESETHAND
將訊號的處理函式在處理函式的入口重置為
SIG_DFL
- SA_RESTART
重啟可中斷的函式而不是給出
EINTR
錯誤 - SA_SIGINFO
使用
sa_sigaction
做為訊號的處理函式 - SA_NODEFER
捕獲到訊號時不將它新增到訊號遮蔽字中
簡單的程式碼示例如下:
#include <stdio.h>
#include <string.h>
#include <signal.h>
#define SIG SIGINT
static void
sig_handler(int sig, siginfo_t *si, void *data)
{
printf("Caught signal: %d
", sig);
printf("Sender pid: %d
", si->si_pid);
printf("Sender uid: %d
", si->si_uid);
}
static int
sig_caught(int sig)
{
printf("Start caught signal: %d
", sig);
struct sigaction sa;
sa.sa_flags = SA_SIGINFO;
sa.sa_sigaction = sig_handler;
sigemptyset(&sa.sa_mask);
int ret = sigaction(sig, &sa, NULL);
if (ret == -1) {
printf("Failed to caught signal: %d
", sig);
return -1;
}
return 0;
}
int
main(int argc, char *argv[])
{
if (sig_caught(SIG) == -1) {
return -1;
}
printf("Caught signal(%d), input `quit` to exit...
", SIG);
char buf[1024] = {0};
while(1) {
printf("Please input: ");
scanf("%s", buf);
if (strcmp(buf, "quit") == 0) {
break;
}
}
printf("Exit...
");
return 0;
}
訊號遮蔽字
考慮一下這種情況:在 signal()/sigaction()
返回之前程式就已經收到了需要處理的訊號,此時程式會以預設行為來處理,這顯然不符合我們的期望。 這時就需要用到訊號遮蔽字了,在程式啟動時就將需要處理的訊號加入的遮蔽字中,等 signal()/sigaction()
返回後再解除遮蔽,解除遮蔽後至少會將收到的待處理訊號傳送一個給程式。
遮蔽字用到一下函式:
int sigemptyset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigprocmask(int how, const sigset_t *restrict set,
sigset_t *restrict oset);
sigprocmask
中 set
為需要設定的遮蔽字集, oset
為之前的遮蔽字集, how
控制著 set
如何生效,可設定為以下值:
- SIG_BLOCK
該程式的遮蔽字集將為當期遮蔽字集與
set
的並集,set
中包含了需要遮蔽的訊號集 - SIG_UNBLOCK
該程式的遮蔽字集將為當期遮蔽字集與
set
的補集的交集,set
中包含了需要解除遮蔽的訊號集 - SIG_SETMASK
該程式的遮蔽字集將設定為
set
的值
簡單的設定流程如下:
int
sig_block(int sig, int how)
{
sigset_t mask;
sigemptyset(&mask)
sigaddset(&mask, sig);
sigprocmask(how, &mask, NULL);
}
訊號傳送
訊號可以通過 kill
函式傳送給指定程式,也可以通過 raise
或者 alarm
函式傳送給當前執行的執行緒或程式,下面來分別說說這幾個函式。
kill
原型:
int kill(pid_t pid, int sig);
kill
函式向指定程式傳送指定的訊號,如果訊號為 0 將執行錯誤檢查,訊號並不會傳送,可以用來檢查 pid
的有效性。
pid
大於 0 時訊號將傳送給此程式, pid
小於等於 0 時,如下:
- 等於 0
訊號將傳送給傳送者所在組裡的所有程式
- 等於 -1
訊號將傳送給所有程式
- 小於 -1
訊號將傳送給程式組為
pid
絕對值的所有組內程式
alarm
原型:
unsigned alarm(unsigned seconds);
alarm
函式將在指定的 seconds
之後傳送一個 SIGALRM
訊號,如果 seconds
為 0, 則取消之前的定時器請求。如果不為 0 則取消之前的請求,重新設定為 seconds
。 如果在等待結束之前有其他的事件產生,那定時器請求也將被取消。
簡單的程式碼示例如下:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
static void
handler(int sig)
{
printf("alarm arrived: %d
", sig);
}
int
main(int argc, char *argv[])
{
signal(SIGALRM, handler);
alarm(2);
sleep(2);
printf("alarm 5s over
");
alarm(10);
sleep(1);
unsigned int remaining = alarm(3);
printf("alarm 10s remain: %u, reset to 3
", remaining);
sleep(3);
printf("alarm 3s over
");
alarm(20);
sleep(3);
remaining = alarm(0);
printf("cancel alarm 20s, remian: %u, exit...
", remaining);
}
raise
原型:
int raise(int sig);
raise
函式將給當前執行的執行緒或程式傳送訊號,如果訊號處理函式已經被呼叫, raise
函式將等待訊號處理函式呼叫結束才返回。
結語
訊號處理函式是會被重複呼叫的,所以必要儲存其是可重入的,注意處理邏輯。
另外本文中的程式碼都在 signal 中,這個 repo
也有其它的示例,有興趣的可以看看。
附錄
訊號表
/* ISO C99 signals. */
#define SIGINT 2 /* Interactive attention signal. */
#define SIGILL 4 /* Illegal instruction. */
#define SIGABRT 6 /* Abnormal termination. */
#define SIGFPE 8 /* Erroneous arithmetic operation. */
#define SIGSEGV 11 /* Invalid access to storage. */
#define SIGTERM 15 /* Termination request. */
/* Historical signals specified by POSIX. */
#define SIGHUP 1 /* Hangup. */
#define SIGQUIT 3 /* Quit. */
#define SIGTRAP 5 /* Trace/breakpoint trap. */
#define SIGKILL 9 /* Killed. */
#define SIGBUS 10 /* Bus error. */
#define SIGSYS 12 /* Bad system call. */
#define SIGPIPE 13 /* Broken pipe. */
#define SIGALRM 14 /* Alarm clock. */
/* New(er) POSIX signals (1003.1-2008, 1003.1-2013). */
#define SIGURG 16 /* Urgent data is available at a socket. */
#define SIGSTOP 17 /* Stop, unblockable. */
#define SIGTSTP 18 /* Keyboard stop. */
#define SIGCONT 19 /* Continue. */
#define SIGCHLD 20 /* Child terminated or stopped. */
#define SIGTTIN 21 /* Background read from control terminal. */
#define SIGTTOU 22 /* Background write to control terminal. */
#define SIGPOLL 23 /* Pollable event occurred (System V). */
#define SIGXCPU 24 /* CPU time limit exceeded. */
#define SIGXFSZ 25 /* File size limit exceeded. */
#define SIGVTALRM 26 /* Virtual timer expired. */
#define SIGPROF 27 /* Profiling timer expired. */
#define SIGUSR1 30 /* User-defined signal 1. */
#define SIGUSR2 31 /* User-defined signal 2. */
/* Nonstandard signals found in all modern POSIX systems
(including both BSD and Linux). */
#define SIGWINCH 28 /* Window size change (4.3 BSD, Sun). */
/* Archaic names for compatibility. */
#define SIGIO SIGPOLL /* I/O now possible (4.2 BSD). */
#define SIGIOT SIGABRT /* IOT instruction, abort() on a PDP-11. */
#define SIGCLD SIGCHLD /* Old System V name */
/* Not all systems support real-time signals. bits/signum.h indicates
that they are supported by overriding __SIGRTMAX to a value greater
than __SIGRTMIN. These constants give the kernel-level hard limits,
but some real-time signals may be used internally by glibc. Do not
use these constants in application code; use SIGRTMIN and SIGRTMAX
(defined in signal.h) instead. */
#define __SIGRTMIN 32
#define __SIGRTMAX __SIGRTMIN
/* Biggest signal number + 1 (including real-time signals). */
#define _NSIG (__SIGRTMAX + 1)