Linux系统 信号
- 发送信号
- 1. 进程组
- 2. /bin/kill 程序发送信号
- 3. 键盘发送信号
- 4. 用kill函数发送信号
- 5. 用alarm函数发送信号
- 接收信号
- 阻塞和解除阻塞信号
- 编写信号处理程序
- 显式等待信号
信号是一种软件中断形式,用于通知进程发生了特定事件。它是进程间异步通信的一种方式,意味着信号可以在进程执行的任意时刻到达,而进程不一定正在等待该信号。例如,当用户在终端按下 Ctrl+C 时,内核会向当前正在运行的进程发送一个SIGINT
信号,通知进程用户希望中断其执行。
序号 | 名称 | 默认行为 | 相应事件 |
---|---|---|---|
1 | SIGHUP | 终止 | 终端线挂断 |
2 | SIGINT | 终止 | 来自键盘的中断 |
3 | SIGQUIT | 终止 | 来自键盘的退出 |
4 | SIGILL | 终止 | 非法指令 |
5 | SIGTRAP | 终止并转储内存 | 跟踪陷阱 |
6 | SIGABRT | 终止并转储内存 | 来自 abort 函数的终止信号 |
7 | SIGBUS | 终止 | 总线错误 |
8 | SIGFPE | 终止并转储内存 | 浮点异常 |
9 | SIGKILL | 终止(不能被捕获和忽略) | 杀死程序 |
10 | SIGUSR1 | 终止 | 用户定义的信号 1 |
11 | SIGSEGV | 终止并转储内存 | 无效的内存引用(段故障) |
12 | SIGUSR2 | 终止 | 用户定义的信号 2 |
13 | SIGPIPE | 终止 | 向一个没有读用户的管道做写操作 |
14 | SIGALRM | 终止 | 来自 alarm 函数的定时器信号 |
15 | SIGTERM | 终止 | 软件终止信号 |
16 | SIGSTKFLT | 终止 | 协处理器上的栈故障 |
17 | SIGCHLD | 忽略 | 一个子进程停止或者终止 |
18 | SIGCONT | 忽略 | 继续进程如果该进程停止 |
19 | SIGSTOP | 停止直到下一个SIGCONT(不能被捕获和忽略) | 不是来自终端的停止信号 |
20 | SIGTSTP | 停止直到下一个SIGCONT | 来自终端的停止信号 |
21 | SIGTTIN | 停止直到下一个SIGCONT | 后台进程从终端读 |
22 | SIGTTOU | 停止直到下一个SIGCONT | 后台进程向终端写 |
23 | SIGURG | 忽略 | 套接字上的紧急情况 |
24 | SIGXCPU | 终止 | CPU 时间限制超出 |
25 | SIGXFSZ | 终止 | 文件大小限制超出 |
26 | SIGVTALRM | 终止 | 虚拟定时器期满 |
27 | SIGPROF | 终止 | 剖析定时器期满 |
28 | SIGWINCH | 忽略 | 窗口大小变化 |
29 | SIGIO | 终止 | 在某个描述符上可执行 I/O 操作 |
30 | SIGPWR | 终止 | 电源故障 |
传送一个信号到目的进程是由两个不同步骤组成的:
-
发送信号:内核通过更新目的进程上下文中的某个状态,发送一个信号给目的进程。发送信号可以有如下两种原因:
- 1)内核检测到一个系统事件,比如除零错误或者子进程终止。
- 2)一个进程调用了 kill 函数(在下一节中讨论),显式地要求内核发送一个信号给目的进程。一个进程可以发送信号给它自己。
-
接收信号:当目的进程被内核强迫以某种方式对信号的发送做出反应时,它就接收了信号。进程可以忽略这个信号,终止或者通过执行一个称为信号处理程序(signal handler)的用户层函数捕获这个信号。图 8-27 给出了信号处理程序捕获信号的基本思想。
待处理信号:每种信号类型在同一时刻至多有一个待处理信号,新发送的同类型信号若已有待处理信号则会被丢弃。内核通过pending
位向量记录待处理信号,信号传送时设置相应位,接收后清除该位。
信号阻塞机制:进程可选择性阻塞信号,被阻塞信号仍可发送但产生的待处理信号暂不被接收。内核用blocked
位向量维护被阻塞信号,进程可操作此位向量控制信号阻塞状态,从而灵活管理信号接收时机,有助于系统资源管理和进程执行的稳定性与可预测性。
发送信号
Unix 系统提供了大量向进程发送信号的机制。所有这些机制都是基于进程组(process group)这个概念的。
1. 进程组
每个进程都只属于一个进程组,进程组是由一个正整数进程组 ID 来标识的。
#include <unistd.h>pid_t getpgrp(void);
// 返回:调用进程的进程组 ID。
getpgrp
用于获取调用进程所属的进程组 ID。
#include <unistd.h>int setpgid(pid_t pid, pid_t pgid);
// 返回:若成功则为o,若错误则为 -1。
setpgid
用于设置进程的进程组 ID,从而将进程加入某个进程组或创建新的进程组。
2. /bin/kill 程序发送信号
使用完整路径 /bin/kill
,因为有些 Unix shell 有自己内置的 kill 命令。
/bin/kill -9 15213
发送信号9(SIGKILL)给进程15213。
/bin/kill -9 -15213
发送信号9(SIGKILL)给进程组15213的每个进程。
3. 键盘发送信号
ls | sort
会创建一个由两个进程组成的前台作业,这两个进程是通过 Unix 管道连接起来的:一个进程运行 ls 程序,另一个运行 sort 程序。shell 为每个作业创建一个独立的进程组。进程组 ID 通常取自作业中父进程中的一个。
在键盘上输入 Ctrl+C 会导致内核发送一个 SIGINT 信号到前台进程组中的每个进程,会终止前台作业。输入 Ctrl+Z 会发送一个 SIGTSTP 信号到前台进程组中的每个进程,会停止(挂起)前台作业。
4. 用kill函数发送信号
#include <sys/types.h>
#include <signal.h>int kill(pid_t pid, int sig);
// 返回:若成功则为 0,若错误则为 -1。
kill
用于向指定的进程或进程组发送信号。
pid > 0
:信号发送给指定 PID 的进程。pid = 0
:信号发送给与调用进程在同一进程组的所有进程。pid < 0
:信号发送给进程组 ID 为-pid
的所有进程。pid = -1
:信号发送给调用进程有权限发送的所有进程。
5. 用alarm函数发送信号
#include <unistd.h>unsigned int alarm(unsigned int secs);
// 返回:前一次闹钟剩余的秒数,若以前没有设定闹钟,则为0。
alarm
用于设置一个定时器,在指定时间后向当前进程发送 SIGALRM
信号。
接收信号
内核在将进程从内核模式切换到用户模式时(如系统调用返回或上下文切换完成后)检查进程未被阻塞的待处理信号集合(通过**pending &~blocked
**计算得出)。若集合为空,进程正常执行下一条指令;若不为空,内核选择一个信号(通常是最小的k
)让进程接收。每个信号类型都有一个预定义的默认行为,是下面中的一种:
- 进程终止。
- 进程终止并转储内存。
- 进程停止(挂起)直到被 SIGCONT 信号重启。
- 进程忽略该信号。
#include <signal.h>
typedef void (*sighandler_t)(int);sighandler_t signal(int signum, sighandler_t handler);
// 返回:若成功则为指向前次处理程序的指针,若出错则为 SIG_ERR(不设置 errno)。
signum
要处理的信号编号,例如 SIGINT
(Ctrl+C)触发。
handler
指定对信号的处理方式,有以下几种:
SIG_DFL
:恢复信号的默认行为。SIG_IGN
:忽略信号。- 用户定义的信号处理函数,接受作为参数的函数指针。
// 用信号处理程序捕获SIGINT信号的程序void signt_handler(int sig)
{printf("Caught SIGINT\n");exit(0);
}int main()
{// Install the SIGINT handlerif (signal(SIGINT, signt_handler) == SIG_ERR)unix_error("signal error");Pause();return 0;
}
阻塞和解除阻塞信号
Linux 提供阻塞信号的隐式和显式的机制:
- 隐式阻塞机制:内核默认阻塞任何当前处理程序正在处理信号类型的待处理的信号。
- 显式阻塞机制:应用程序可以使用 sigprocmask 函数和它的辅助函数,明确地阻塞和解除阻塞选定的信号。
#include <signal.h>int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
//返回;如果成功则为 0,若出错则为 -1。int sigismember(const sigset_t *set, int signum);
// 返回:若 signum 是 set 的成员则为 1,如果不是则为 0,若出错则为 -1。
sigprocmask
: 函数改变当前阻塞的信号集合。如果 oldset 非空,那么 blocked 位向量之前的值保存在 oldset 中。具体的行为依赖于 how 的值:- 阻塞信号:通过
SIG_BLOCK
防止某些信号被处理。 - 解除阻塞:通过
SIG_UNBLOCK
恢复信号处理。 - 替换信号屏蔽字:通过
SIG_SETMASK
设置新的信号屏蔽字。
- 阻塞信号:通过
sigemptyset
: 初始化 set 为空集合。sigfillset
: 把每个信号都添加到 set 中。sigaddset
: 把 signum 添加到 set。sigdelset
: 从 set 中删除 signum。sigismember
:检查信号是否在信号集中,如果 signum 是 set 的成员,那么 sigismember 返回 1,否则返回 0。
sigprocmask 来临时阻塞接收 SIGINT 信号的方法。
sigset_t mask, prev_mask;Sigemptyset(&mask);
Sigaddset(&mask, SIGINT);/* Block SIGINT and save previous blocked set */
Sigprocmask(SIG_BLOCK, &mask, &prev_mask);// code...// Code region that will not be interrupted by SIGINT/* Restore previous blocked set, unblocking SIGINT */
Sigprocmask(SIG_SETMASK, &prev_mask, NULL);
编写信号处理程序
信号处理是 Linux 系统编程最棘手的一个问题。处理程序有几个属性使得它们很难推理分析:
- 处理程序与主程序并发运行,共享同样的全局变量,因此可能与主程序和其他处理程序互相干扰。
- 如何以及何时接收信号的规则常常有违人的直觉。
- 不同的系统有不同的信号处理语义。
保守的安全的信号处理的原则:G0. 处理程序要尽可能简单。G1. 在处理程序中只调用异步信号安全的函数。G2. 保存和恢复 errno。G3. 阻塞所有的信号,保护对共享全局数据结构的访问。G4. 用 volatile 声明全局变量。G5. 用 sig_atomic_t 声明标志。
信号处理程序中产生输出唯一安全的方法是使用 write 函数。调用 printf 或 sprintf 是不安全的。为了绕开这个限制,我们开发一些安全的函数,称为 SIO(安全的 I/O)包,可以用来在信号处理程序中打印简单的消息。可以从 CS:APP 网站上在线地得到这些代码。
#include "csapp.h"ssize_t sio_putl(long v);
ssize_t sio_puts(char s[]);
// 返回:如果成功则为传送的字节数,如果出错,则为 -1。void sio_error(char s[]);
// 返回:空。ssize_t sio_puts(char s[]) /* Put string */
{return write(STDOUT_FILENO, s, sio_strlen(s));
}ssize_t sio_putl(long v) /* Put long */
{char s[128];sio_ltoa(v, s, 10); /* Based on K&R itoa() */return sio_puts(s);
}void sio_error(char s[]) /* Put error message and exit */
{sio_puts(s);_exit(1);
}
void handler(int sig)
{int olderrno = errno;if (waitpid(-1, NULL, 0) < 0)sio_error("waitpid error");sio_puts("Hanler reaped child\n");sleep(1);errno = olderrno;
}int main()
{int i, n;char buf[MAXBUF];if (signal(SIGCHLD, handler) == SIG_ERR)unix_error("signal error");for (int i = 0; i < 3; i++) {if (Fork() == 0) {printf("Hello from child %d\n", (int)getpid());exit(0);}}// Parents waits for terminal input and then processes itif ((n = read(STDERR_FILENO, buf, sizeof(buf))) < 0)unix_error("read");printf("Parent processing input\n");while(1);return 0;
}
当在 Linux 系统上运行它时,我们得到如下输出:
./signal2
Hello from child 73020
Hello from child 73019
Hanler reaped child
Hello from child 73021
Hanler reaped child
尽管发送了 3 个 SIGCHLD 信号给父进程,但是其中只有两个信号被接收了,因此父进程只是回收了两个子进程。如果挂起父进程,我们看到,实际上子进程 73021 没有被回收,它成了一个僵死进程(在 ps 命令的输出中由字符串 “defunct” 表明)。
ps axj | grep signal1901 73018 73018 901 pts/4 73018 S+ 1000 0:00 ./signal173018 73021 73018 901 pts/4 73018 Z+ 1000 0:00 [signal1] <defunct>
父进程先接收并捕获第一个信号,在处理该信号时,第二个信号传送过来但因 SIGCHLD 信号被其处理程序阻塞而未被接收,随后第三个 SIGCHLD 信号到达,因已有待处理的 SIGCHLD 信号,它被丢弃。之后处理程序返回,内核让父进程接收待处理的 SIGCHLD 信号,父进程再次执行处理程序处理第二个信号,处理完后无待处理的 SIGCHLD 信号且第三个 SIGCHLD 信号信息已丢失。不能用信号对其他进程中发生的事件计数。
下面是改进版本,能够正确解决信号不会排队等待的情况
void handler2(int sig)
{int olderrno = errno;while (waitpid(-1, NULL, 0) > 0)sio_puts("Hanler reaped child\n");if (errno != ECHILD)sio_error("waitpid error");sleep(1);errno = olderrno;
}
./signal2
Hello from child 75801
Hello from child 75802
Hanler reaped child
Hanler reaped child
Hello from child 75803
Hanler reaped child
显式等待信号
#include "csapp.h"volatile sig_atomic_t pid;void sigchld_handler(int s)
{int olderrno = errno;pid = Waitpid(-1, NULL, 0);errno = olderrno;
}void sigint_handler(int s)
{
}int main(int argc, char **argv)
{sigset_t mask, prev;Signal(SIGCHLD, sigchld_handler);Signal(SIGINT, sigint_handler);Sigemptyset(&mask);Sigaddset(&mask, SIGCHLD);while (1) {Sigprocmask(SIG_BLOCK, &mask, &prev); /* Block SIGCHLD */if (Fork() == 0) /* Child */exit(0);/* Wait for SIGCHLD to be received */pid = 0;while (!pid)sigsuspend(&prev);/* Optionally unblock SIGCHLD */Sigprocmask(SIG_SETMASK, &prev, NULL);/* Do some work after receiving SIGCHLD */printf(".");}exit(0);
}
父进程先注册 SIGCHLD 和 SIGINT 信号处理函数,接着阻塞 SIGCHLD 信号后创建子进程(子进程立即退出),然后通过 sigsuspend
挂起等待 SIGCHLD 信号,收到信号后由 sigchld_handler
处理程序回收子进程,之后恢复原来的信号屏蔽字,最后打印 “.” 表示完成子进程处理并继续后续操作。