Linux信号(signal)允许进程和内核中断其它进程,例如
Ctrl+C
会向终端中正在运行的进程发送
SIGINT
信号,一个进程试图除0时内核会发送给它一个
SIGFPE
信号,进程执行一条非法指令时内核会发送
SIGILL
信号,进程非法内存引用时内核会发送给它一个
SIGSEGV
信号,一个进程可以通过向另一个进程发送
SIGKILL
信号强制终止它,当一个子进程终止时,内核会发送
SIGCHLD
信号给其父进程。下表展示了Linux系统支持的30多种不同类型的信号。
Signal | Standard | Action | Comment |
---|---|---|---|
SIGABRT | P1990 | 终止并转储内存 | Abort signal from abort(3) |
SIGALRM | P1990 | 终止 | Timer signal from alarm(2) |
SIGBUS | P2001 | 终止并转储内存 | Bus error (bad memory access) |
SIGCHLD | P1990 | 忽略 | Child stopped or terminated |
SIGCLD | - | 忽略 | A synonym for SIGCHLD |
SIGCONT | P1990 | Cont | Continue if stopped |
SIGEMT | - | 终止 | Emulator trap |
SIGFPE | P1990 | 终止并转储内存 | Floating-point exception |
SIGHUP | P1990 | 终止 | Hangup detected on controlling terminal or death of controlling processss |
SIGILL | P1990 | 终止并转储内存 | Illegal Instruction |
SIGINFO | - | A synonym for SIGPWR | |
SIGINT | P1990 | 终止 | Interrupt from keyboard |
SIGIO | - | 终止 | I/O now possible (4.2BSD) |
SIGIOT | - | 终止并转储内存 | IOT trap. A synonym for SIGABRT |
SIGKILL | P1990 | 终止 | Kill signal |
SIGLOST | - | 终止 | File lock lost (unused) |
SIGPIPE | P1990 | 终止 | Broken pipe: write to pipe with no readers; see pipe(7) |
SIGPOLL | P2001 | 终止 | Pollable event (Sys V). Synonym for SIGIO |
SIGPROF | P2001 | 终止 | Profiling timer expired |
SIGPWR | - | 终止 | Power failure (System V) |
SIGQUIT | P1990 | 终止并转储内存 | Quit from keyboard |
SIGSEGV | P1990 | 终止并转储内存 | Invalid memory reference |
SIGSTKFLT | - | 终止 | Stack fault on coprocessor (unused) |
SIGSTOP | P1990 | 停止直到下一个SIGCONT | Stop process |
SIGTSTP | P1990 | 停止直到下一个SIGCONT | Stop typed at terminal |
SIGSYS | P2001 | 终止并转储内存 | Bad system call (SVr4); see also seccomp(2) |
SIGTERM | P1990 | 终止 | Termination signal |
SIGTRAP | P2001 | 终止并转储内存 | Trace/breakpoint trap |
SIGTTIN | P1990 | 停止直到下一个SIGCONT | Terminal input for background process |
SIGTTOU | P1990 | 停止直到下一个SIGCONT | Terminal output for background process |
SIGUNUSED | - | 终止并转储内存 | Synonymous with SIGSYS |
SIGURG | P2001 | 忽略 | Urgent condition on socket (4.2BSD) |
SIGUSR1 | P1990 | 终止 | User-defined signal 1 |
SIGUSR2 | P1990 | 终止 | User-defined signal 2 |
SIGVTALRM | P2001 | 终止 | Virtual alarm clock (4.2BSD) |
SIGXCPU | P2001 | 终止并转储内存 | CPU time limit exceeded (4.2BSD); see setrlimit(2) |
SIGXFSZ | P2001 | 终止并转储内存 | File size limit exceeded (4.2BSD); see setrlimit(2) |
SIGWINCH | - | 忽略 | Window resize signal (4.3BSD, Sun) |
注:上述信号中SIGSTOP
与SIGKILL
既不能被捕获也不能被忽略。
术语
- 发送信号。内核通过更新目的进程上下文的某个状态发送一个信号给目的进程。内核可能因为:1)检测到一个系统事件。2)一个进程调用了
kill
显示地要求内核发送一个信号给目的进程。进程可以给自己发信号。 - 接收信号。目的进程对信号做出反应时,它就接收了信号。进程可以忽略这个信号,或者通过执行信号处理程序捕获这个信号。
- 待处理信号(pending signal)。发出而没有被接收的信号称之为待处理信号。在任何时刻,一个进程的某种类型的待处理信号至多有一个。
- 阻塞信号(blocked signal)。进程可以阻塞接收某种信号,这种情况下该信号仍然是待处理信号,但直到进程取消阻塞前,它都不会被进程接收。
进程组
每个进程都只属于一个进程组,下面函数可以用来查看当前进程的进程组以及改变自己或其它进程的进程组:
#include <unistd.h>
pid_t getpgrp();
// 返回:调用进程的进程组ID
pid_t setpgid(pid_t pid, pid_t pgid);
// 返回:若成功则为0,若错误则为-1
对于setpgid
来说,若pid
为0,那么就是用当前进程的PID,如果pgid
为0,那么就用pid
指定的进程的PID作为进程组的ID。
发送信号
- 使用
/bin/kill
命令发送信号
例如kill -9 15213
会发送信号9SIGKILL
终止该进程 - 通过键盘发送信号
Ctrl+C发送SIGINT
信号,默认情况下终止进程;Ctrl+Z发送SIGTSTP
信号,默认情况下挂起当前进程 - 使用
kill
函数发送信号#include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig); // 返回:若成功则为0,若错误则为-1
- pid > 0,发送信号sig给进程pid
- pid == 0,发送信号sig给调用进程所在进程组的每个进程,包括调用进程
- pid < 0,发送信号sig给进程组|pid|(绝对值)中的每个进程。
- 使用
alarm
函数发送信号#include <unistd.h> unsigned int alarm(unsigned int secs); // 返回:前一次闹钟剩余的秒数,若以前没有设定闹钟,则为0
接收信号
每种信号类型都有下面四种行为中的一个默认行为:
- 进程终止
- 进程终止并转储内存
- 进程挂起直到被
SIGCONT
信号重启 - 忽略该信号
进程可以通过signal
函数设置收到信号的行为(SIGSTOP
和SIGKILL
除外):
#include <signal.h>
typedef void (*sighandler_t)(int); // 函数指针
sighandler_t signal(int signum, sighandler_t handler);
// 返回:若成功则为指向前次处理程序的指针,若出错则为SIG_ERR (不设置errno)
对于第二个参数handler
来说:
SIG_IGN
,忽略signum
类型的信号SIG_DFL
,恢复signum
类型信号的默认行为- 函数指针,这个函数即为
signum
信号的处理程序,进程收到该信号时会执行处理程序(即handler
函数);处理程序返回(return语句)时,指令通常会回到程序被信号中断的位置。
阻塞及解除阻塞信号
- 隐式阻塞机制
默认阻塞与正在处理信号同类型的信号 - 显式阻塞机制
进程可以使用sigprocmask
及其辅助函数阻塞多个信号:
对于#include <signal.h> int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); // 使用set设置要阻塞的信号,how指明行为,oldset存放旧的set int sigemptyset(sigset_t *set); // 置set为空 int sigfillset(sigset_t *set); // 将每个信号都加入set中 int sigaddset(sigset_t *set, int signum); // 将信号signum加入set int sigdelset(sigset_t *set, int signum); // 从set中删除signum // 返回:若成功则为0,若错误则为-1 int sigismember(const sigset_t *set, int signum); // 返回:若signum是set的一员,则为1,如果不是则为0,若出错则为-1
sigprocmask
来说,其行为依赖于第一个参数how
的值:SIG_BLOCK
: 把set
中的信号添加到阻塞信号集blocked中SIG_UNBLOCK
: 从阻塞信号集blocked中删除set
中的信号SIG_SETMASK
: 设置阻塞信号集为set
如下程序展示了使用方法:
sigset_t mask, prev; sigemptyset(&mask); sigaddset(&mask, SIGINT); sigprocmask(SIG_BLOCK, &mask, &prev); // 阻塞mask中的信号SIGINT //...程序不会被 SIGINT 信号打断 sigprocmask(SIG_SETMASK, &prev, NULL); // 恢复程序的阻塞信号集
注:阻塞信号时,信号仍能发送到进程并成为待处理信号,但会阻塞而不是被立即处理。
等待信号
可以使用sigsuspend
函数挂起当前进程并等待某一个信号:
#include <signal.h>
int sigsuspend(const sigset_t *mask);
// 返回:-1
该函数首先使用mask
替换当前的阻塞集合,然后挂起进程直到进程收到一个信号。该函数可以用来避免一些多进程并发中的同步问题,例如对于如下程序:
void handler(int sig) {
printf("signal SIGUSR1 got.\n");
}
int main(int argc, char *argv[]) {
printf("here we go...\n");
signal(SIGUSR1, handler);
pid_t pid = fork();
if (pid == 0) {
printf("child: I will do something.\n");
kill(getppid(), SIGUSR1);
printf("child: I will do some other things and exit.\n");
exit(0);
}
// 希望在子进程发送SIGUSR1信号后执行的任务
printf("parent: I will do something after child send SIGUSR1\n");
wait(NULL); // recycle child
return 0;
}
上述程序,父进程希望在子进程发送SIGUSR1信号后执行一些任务,由于父子进程并发执行,因此无法断定何时子进程发送信号。虽然可以使用sleep
或pause
函数使父进程休眠,但这种方式并不妥当,并且子进程可能在父进程调用pause
前就发送信号。比如上述程序的输出:
here we go...
parent: I will do something after child send SIGUSR1
child: I will do something.
child: I will do some other things and exit.
signal SIGUSR1 got.
解决方法是使用sigprocmask
和sigsuspend
函数,如下为修改后的main
函数:
int main(int argc, char *argv[]) {
printf("here we go...\n");
signal(SIGUSR1, handler);
sigset_t mask, prev;
sigemptyset(&mask);
sigaddset(&mask, SIGUSR1); // 阻塞SIGUSR1信号
sigprocmask(SIG_BLOCK, &mask, &prev);
pid_t pid = fork();
if (pid == 0) {
sigprocmask(SIG_SETMASK, &prev, NULL); // 子进程继承父进程阻塞集,因此要恢复子进程的阻塞集合
printf("child: I will do something.\n");
kill(getppid(), SIGUSR1);
printf("child: I will do some other things and exit.\n");
exit(0);
}
sigsuspend(&prev); // 取消阻塞,挂起进程并等待信号
printf("parent: I will do something after child send SIGUSR1\n");
sigprocmask(SIG_SETMASK, &prev, NULL);
wait(NULL); // recycle child
return 0;
}
可以看到,父进程首先阻塞了SIGUSR1
信号,之后运行子进程,此时即便子进程先调度并给父进程发送SIGUSR1
信号,也会加入父进程的待处理信号集合中。之后父进程调用sigsuspend
函数,取消阻塞、挂起进程并等待信号,便可以保证在子进程发送SIGUSR1
后再执行接下来的任务。