信号是一种软件终端,提供了一种处理异步事件的方法,也是进程间通信的唯一一个异步的通信方式。Unix中定义了很多信号,有很多条件可以产生信号,对于这些信号有不同的处理方式。本文会详细讲述信号的机制。
通过kill -l 可以查看所有的信号定义:
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
信号 | 属性值 | 默认处理方式 | 定义 |
SIGHUP | 1 | Term | Hangup detected on controlling terminal or death of controlling process |
SIGINT | 2 | Term | Interrupt from keyboard |
SIGQUIT | 3 | Core | Quit from keyboard |
SIGILL | 4 | Core | Illegal Instruction |
SIGTRAP | 5 | Core | Trace/breakpoint trap |
SIGABRT | 6 | Core | Abort signal from abort(3) |
SIGBUS | 7 | Core | Bus error (bad memory access) |
SIGFPE | 8 | Core | Floating point exception |
SIGKILL | 9 | Term | Kill signal |
SIGUSR1 | 10 | Term | User-defined signal 1 |
SIGSEGV | 11 | Core | Invalid memory reference |
SIGUSR2 | 12 | Term | User-defined signal 2 |
SIGPIPE | 13 | Term | Broken pipe: write to pipe with no readers |
SIGALRM | 14 | Term | Timer signal from alarm(2) |
SIGTERM | 15 | Term | Termination signal |
SIGSTKFLT | 16 | Term | Stack fault on coprocessor (unused) |
SIGCHLD | 17 | Ign | Child stopped or terminated |
SIGCONT | 18 | Cont | Continue if stopped |
SIGSTOP | 19 | Stop | Stop process |
SIGTSTP | 20 | Stop | Stop typed at terminal |
SIGTTIN | 21 | Stop | Terminal input for background process |
SIGTTOU | 22 | Stop | Terminal output for background process |
SIGURG | 23 | Ign | Urgent condition on socket (4.2BSD) |
SIGXCPU | 24 | Core | CPU time limit exceeded (4.2BSD) |
SIGXFSZ | 25 | Core | File size limit exceeded (4.2BSD) |
SIGVTALRM | 26 | Term | Virtual alarm clock (4.2BSD) |
SIGPROF | 27 | Term | Profiling timer expired |
SIGWINCH | 28 | Ign | Window resize signal (4.3BSD, Sun) |
SIGIO | 29 | Term | I/O now possible (4.2BSD) |
SIGPWR | 30 | Term | Power failure (System V) |
SIGUNUSED | 31 | Core | Synonymous with SIGSYS |
表1 标准信号
表中处理方式:
注意:
硬件方式
软件方式
4.1 kill 函数
#include
int kill (pid_t pid, int sig);
将信号sig 发送给pid 进程;
参数pid:
返回值:
失败原因:
4.2 killpg 函数
#include
int killpg (pid_t pgrp, int sig);
发送信号sig 到pgrp 的所有进程中;如果pgrp 为0,则发送sig 给当前调用该函数所属group里所有的进程;
4.3 raise 函数
#include
int raise (int sig);
给当前进程发送信号sig;
等价于:
kill(getpid(), sig);
4.4 alarm 函数
#include
unsigned int alarm(unsigned int seconds);
alarm函数可以用来设置定时器,定时器超时将产生SIGALRM信号给调用进程。
参数seconds表示设定的秒数,经过seconds后,内核将给调用该函数的进程发送SIGALRM信号。
对于产生的信号有三种方式处理:
大多数信号都可以用这种方式处理,但有两个信号决不能被忽略,SIGKILL 和SIGSTOP,这两个信号向超级用户提供了使进程终止或停止的可靠方法。
可以通过函数将捕捉的函数通知给内核,在收到信号时,如果用户有捕捉的处理,内核会通知捕捉函数处理,处理完成后返回到内核中。下面会详细说明捕捉信号的机制。
上面表格1中详细列出
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,
举例如下:
1. 用户程序注册了SIGQUIT信号的处理函数sighandler
2. 当前正在执行main函数,这时发生中断或异常切换到内核态
3. 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达
4. 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
(By default, the signal handler is invoked on the normal process stack. It is possible to arrange that the signal handler uses an alternate stack; see sigaltstack(2) for a discussion of how to do this and when it might be useful.)
5. sighandler 函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。
6. 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
上图出自ULK。
7.1 信号在内核中的表示
内核中没一个进程都会对应 3 张表,每个信号都有3中状态:
每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示信号的处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
7.2 三张表的存储
7.3 分析上图中的信号
考虑一个问题,上面一节中讲到block 和pending 表,分别中一个bit位代表一个signal。那如果这个信号在解除阻塞之前已经产生了很多次,那将如何呢?
每个进程都有一个信号屏蔽字(signal mask),它规定了当前要阻塞递送到该进程的信号集。对于每种可能的信号,该屏蔽字都有一位与之对应。对于某种信号,若其对应位已设置,则它当前是被阻塞的。进程可以调用sigprocmask来检测和更改当前信号屏蔽字。POSIX.1 定义了一个新数据类型sigset_t,用于保存一个信号集。
这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞。而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。 阻塞信号集也叫做当前进程的 信号屏蔽字 (Signal Mask),这里的 “屏蔽”应该理解为阻塞而不是忽略。
8.1 信号集操作函数
信号集类型:sigsize_t。sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储,从使用者的角度是不必关心的。这些bit位依赖于系统实现,使用者只能调用以下函数来操作sigset_t变量即可。
#include
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set)
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
注意,在使用sigset_t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。
这四个函数都是成功返回0,出错返回-1。
8.2 sigprocmask
#include
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1。
参数:
#define SIG_BLOCK 0 /* Block signals. */
#define SIG_UNBLOCK 1 /* Unblock signals. */
#define SIG_SETMASK 2 /* Set the set of blocked signals. */
8.3 sigpending
读取当前进程的未决信号集,通过set参数传出。
#include
int sigpending(sigset_t *set);
返回值:调用成功则返回0,出错则返回-1。
9.1 signal 函数
#include
typedef void (*__sighandler_t) (int);
__sighandler_t signal(int signo, __sighandler_t handler);
第一个参数为信号名,详细看表1.
第二个参数为:
#define SIG_ERR ((__sighandler_t) -1) /* Error return. */
#define SIG_DFL ((__sighandler_t) 0) /* Default action. */
#define SIG_IGN ((__sighandler_t) 1) /* Ignore signal. */
返回值:
9.2 sigaction
sigaction函数可以读取和修改与指定信号相关联的处理动作。
#include
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
返回值:
成功返回0,失败返回-1
参数:
sigaction的结构体:
struct sigaction {
union {
__sighandler_t sa_handler;
void (*sa_sigaction) (int, siginfo_t *, void *);
};
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer) (void);
};
举例:
#include
#include
#include
void sig_handle(int sig)
{
puts("recv SIGINT");
sleep(5);
puts("end");
}
int main(int argc, char** argv)
{
struct sigaction act;
act.sa_handler = sig_handle;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGQUIT); //当进入信号处理函数的时候,屏蔽掉SIGQUIT的递达
sigaction(SIGINT, &act, NULL);
while(1)
sleep(1);
return 0;
}
试想一个问题,当进程接收到一个信号时,转到你关联的函数中执行,但是在执行的时候,进程又接收到同一个信号或另一个信号,又要执行相关联的函数时,程序会怎么执行?
也就是说,信号处理函数可以在其执行期间被中断并被再次调用。当返回到第一次调用时,它能否继续正确操作是很关键的。这不仅仅是递归的问题,而是可重入的(即可以完全地进入和再次执行)的问题。而反观Linux,其内核在同一时期负责处理多个设备的中断服务例程就需要可重入的,因为优先级更高的中断可能会在同一段代码的执行期间“插入”进来。
简言之,就是说,我们的信号处理函数要是可重入的,即离开后可再次安全地进入和再次执行,要使信号处理函数是可重入的,则在信息处理函数中不能调用不可重入的函数。下面给出可重入的函数在列表,不在此表中的函数都是不可重入的,可重入函数表如下(man 7 signal可以查看系统中的可重入函数):
accept |
fchmod |
lseek |
sendto |
stat |
access |
fchown |
lstat |
setgid |
symlink |
aio_error |
fcntl |
mkdir |
setpgid |
sysconf |
aio_return |
fdatasync |
mkfifo |
setsid |
tcdrain |
aio_suspend |
fork |
open |
setsockopt |
tcflow |
alarm |
fpathconf |
pathconf |
setuid |
tcflush |
bind |
fstat |
pause |
shutdown |
tcgetattr |
cfgetispeed |
fsync |
pipe |
sigaction |
tcgetpgrp |
cfgetospeed |
ftruncate |
poll |
sigaddset |
tcsendbreak |
cfsetispeed |
getegid |
posix_trace_event |
sigdelset |
tcsetattr |
cfsetospeed |
geteuid |
pselect |
sigemptyset |
tcsetpgrp |
chdir |
getgid |
raise |
sigfillset |
time |
chmod |
getgroups |
read |
sigismember |
timer_getoverrun |
chown |
getpeername |
readlink |
signal |
timer_gettime |
clock_gettime |
getpgrp |
recv |
sigpause |
timer_settime |
close |
getpid |
recvfrom |
sigpending |
times |
connect |
getppid |
recvmsg |
sigprocmask |
umask |
creat |
getsockname |
rename |
sigqueue |
uname |
dup |
getsockopt |
rmdir |
sigset |
unlink |
dup2 |
getuid |
select |
sigsuspend |
utime |
execle |
kill |
sem_post |
sleep |
wait |
execve |
link |
send |
socket |
waitpid |
_Exit & _exit |
listen |
sendmsg |
socketpair |
write |
例如:strtok就是一个不可重入函数,因为strtok内部维护了一个内部静态指针,保存上一次切割到的位置,如果信号的捕捉函数中也去调用strtok函数,则会造成切割字符串混乱,应用strtok_r版本,r表示可重入。
#include
#include
#include
#include
static char buf[] = "hello world good book";
void sig_handle(int sig)
{
strtok(NULL, " ");
}
int main(int argc, char** argv)
{
signal(SIGINT, sig_handle);
printf("%s\n", strtok(buf, " "));
printf("%s\n", strtok(NULL, " "));
sleep(5); //可以被信号打断,返回剩余的时间,想想看这个函数应该怎么调用
printf("%s\n", strtok(NULL, " "));
return 0;
}
运行的时候,发现通过Ctrl+c发射信号与没发射信号的结果不一样,可以改用strtok_r函数。
参考:
https://cloud.tencent.com/developer/article/1008813
相关博文:
进程间通信——序
进程间通信——管道(PIPE)
进程间通信——命名管道(FIFO)
进程间通信——消息队列
进程间通信——共享内存
进程间通信——信号量