信号概述
信号是软件中断。
信号提供了一种处理异步事件的方法。
信号用于大多数复杂的应用程序中。
Unix系统的早期版本就已经提供了信号机制,但不可靠。POSIX.1对可靠信号例程进行了标准化。
信号概述
每个信号都有一个名字,以SIG
开头。
在头文件<signal.h>
中,信号名被定义为正整数常量(信号编号)。
如果应用程序和内核都需使用同一定义,通常将有关信息放置在内核头文件中,然后用户级头文件在包含该内核头文件。
不存在编号为0的信号,POSIX.1将该信号编号值定义为空信号。
产生信号的条件
条件 | 示例 |
---|---|
1. 当用户按某些终端按键时,可引发终端产生信号。 | 在终端上按Delete或Ctrl+C键,通常会产生信号SIGINT ,可用于停止进程。 |
2. 硬件异常产生信号:除数为0、无效的内用引用等。硬件检测到条件通知内核,内核为当时运行的进程产生信号。 | 无效内存引用时,内核为进程产生SIGSEGV 信号。 |
3. 进程调用kill() 函数发送任意信号给另一个进程或进程组。 | 接收信号的进程与发送信号进程的所有者必须相同,或发送信号进程的所有者是超级用户。 |
4. 用户用kill命令信号给其他进程。 | 此命令可以用来终止后台进程。 |
5. 当检测到软件条件发生时,有时也会产生信号通知有关进程。 | SIGURG 带外数据、SIGPIPE 管道读关闭,有进程写、SIGALRM 进程定时器超时。 |
处理信号的方式
- 忽略此信号。(
SIGKILL
与SIGTSOP
信号不能被忽略,这两个信号是内核与超级用户停止进程的可靠方法。) - 捕捉信号。(信号发生时,执行一个用户函数。)不能捕捉
SIGKILL
与SIGSTOP
信号。 - 执行系统默认动作。(对于大多数信号,系统的默认动作是终止该进程。)
信号与默认动作
信号名 | 说明 | 默认动作 |
---|---|---|
SIGABRT | 异常终止 | 进程终止+core |
SIGALRM | 定时器超时 | 进程终止 |
SIGBUS | 硬件故障 | 进程终止+core |
SIGCANCEL | 线程库内部使用 | 忽略 |
SIGCHLD | 子进程状态改变 | 忽略 |
SIGCONT | 使暂停的进程继续 | 进程继续或忽略 |
SIGEMT | 硬件故障 | 进程终止+core |
SIGFPE | 算数异常 | 进程终止+core |
SIGHUP | 连接断开 | 进程终止 |
SIGILL | 非法硬件指令 | 进程终止+core |
SIGINFO | 键盘状态请求 | 忽略 |
SIGINT | 终端中断符 | 进程终止 |
SIGIO | 异步I/O | 进程终止或忽略 |
SIGIOT | 硬件故障 | 进程终止或忽略 |
SIGKILL | 终止 | 进程终止 |
SIGPIPE | 写无读进程的管道 | 进程终止 |
SIGPOLL | 可轮询事件 | 进程终止 |
SIGPWR | 电源失效或重启动 | 进程终止或忽略 |
SIGQUIT | 终端退出符 | 进程终止+core |
SIGSEGV | 无效地址引用 | 进程终止+core |
SIGSTOP | 停止 | 停止进程 |
SIGSYS | 无效系统调用 | 进程终止+core |
SIGTERM | 终止 | 进程终止 |
SIGTSTP | 终端停止/挂起符 | 停止进程 |
SIGTTIN | 后台读控制tty | 停止进程 |
SIGTTOU | 后台写控制tty | 停止进程 |
SIGURG | 紧急情况(套接字) | 忽略 |
SIGUSR1 | 用户自定义信号 | 进程终止 |
SIGUSR2 | 用户自定义信号 | 进程终止 |
SIGXCPU | 超出CPU限制 | 进程终止(+core) |
SIGXFSZ | 超出文件长度限制 | 进程终止(+core) |
Linux中,core文件名可通过/proc/sys/kernel/core_pattern
进行配置。
SIGABRT
调用abort()
函数产生此信号,用于表示进程异常终止。
SIGALRM
当调用alarm()
函数设置的定时器超时会产生此信号,可用于超时限制。
SIGCHLD
当一个进程终止或停止时,会发送SIGCHLD信号给其父进程。
如果父进程想要获取其子进程的状态变化,可以捕捉该信号。信号捕捉函数中通常要调用一种wait
函数以取得子进程ID与其终止状态。
SIGCONT
该作业信号发送给需要继续运行,但当前处于停止状态的进程。
如果接收此信号的进程不是处于停止状态,则忽略该信号。
SIGFPE
算术异常,比如除0、浮点溢出等。
SIGHUP
如果终端接口检测到一个连接断开,则将此信号发送给与该终端相关的控制进程(会话首进程)。
收到此信号的会话首进程也可能是在后台运行,与中断、退出以及挂起信号传递给前台进程组不同。
如果会话首进程终止,也会产生该信号,并且该信号会发送至前台进程组中的每一个进程。
通常用此信号通知守护进程再次读取其配置文件,因为守护进程不会有控制终端。
SIGINT
终端中断信号。
当用户按中断键(Delete或Ctrl+C)时,终端驱动程序产生此信号并发送至前台进程组的每一个进程。
通常用于前台进程失控,向屏幕输出大量信息时,用此信号终止该进程。
SIGKILL
不能被捕捉或忽略的两个信号之一。
用于向系统管理提供杀死进程的可靠方法。
SIGPIPE
当管道的读进程已经终止时,写管道就会产生该信号。
SIGQUIT
终端退出信号。
当用户在终端上按退出键(Ctrl+\)时,终端驱动程序产生此程序并发送给前台进程组中的每一个进程。
此信号会终止前台进程中的所有进程,也会产生一个core文件。
$ ./test
200809
^\Quit (core dumped)
SIGSEGV
进程引用无效内存或内存越界。
SIGSTOP
作业控制信号,用于停止一个进程。
类似于交互停止信号SIGTSTP
,但该信号不能被捕捉或忽略。
SIGSYS
无效的系统调用。
如果用户编写了使用新系统调用的程序,却运行在不支持该系统调用的旧版系统上,则会出现该信号。
SIGTERM
kill命令发送的默认信号,用于终止程序。
该信号可被应用程序捕捉,因此可以让程序做好退出清理工作。
SIGTSTP
交互停止信号。
当用户在终端上按挂起键(Ctrl+Z)时,终端驱动程序会产生此信号,并发送给前台进程组的所有进程。
对于终端驱动程序来说,停止字符一般指的是Ctrl+S终止终端输出,Ctrl+Q启动终端输出。
通常终端驱动程序称产生交互停止信号的字符为挂起字符,而非停止字符。
SIGTTIN
后台进程组进程读取其控制终端时,终端驱动程序会产生该信号。
有两种情况不会产生该信号:
- 该进程忽略或者阻塞此信号。
- 该进程为孤儿进程组(后台进程组)成员,读操作会直接返回出错。
SIGTTOU
后台进程组写其控制终端时,终端驱动程序会产生该信号。
后台进程是否被允许写控制终端是可选择的。
如果不允许后台进程写控制终端,也有两种情况不会产生该信号,与SIGTTIN
信号相同。
SIGURG
此信号用于通知另一个进程发生紧急情况。比如网络连接上收到带外数据,可选择此信号。
SIGUSR1
用户自定义信号,用法取决于应用程序。
SIGUSR2
该信号用法与SIGUSR1
一致。
设置信号处理函数
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
// 返回值:成功返回以前的信号处理函数指针;出错返回SIG_ERR
typedef void (*__sighandler_t) (int);
#define SIG_ERR ((__sighandler_t) -1) /* Error return. */
#define SIG_DFL ((__sighandler_t) 0) /* Default action. */
#define SIG_IGN ((__sighandler_t) 1) /* Ignore signal. */
signum
参数指的是信号名,handler
参数值可以是SIG_IGN
、SIG_DFL
或接收到信号后要执行的函数地址。
signal()
的返回值是一个函数地址,该函数具有一个整型参数,是在此之前信号处理函数的指针。
该函数是由ISO C定义的,因此对于不同的系统具有不同的实现方式。该函数主要用于向后兼容,新程序中建议使用sigaction
函数替代。
示例
下面的示例使用signal()
函数捕捉用户自定义信号SIGUSR1
与SIGUSR2
:
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
static void sig_user(int);
int main(int argc, char* argv[])
{
if (signal(SIGUSR1, sig_user) == SIG_ERR)
perror("signal SIGUSR1");
else if (signal(SIGUSR2, sig_user) == SIG_ERR)
perror("signal SIGUSR2");
while (1)
pause();
}
static void sig_user(int signo)
{
if (signo == SIGUSR1)
printf("received SIGUSR1\n");
else if (signo == SIGUSR2)
printf("received SIGUSR2\n");
else
printf("received signal %d\n", signo);
}
在后台运行该进程,并使用kill
命令将信号发送给它。kill
默认发送SIGTERM
信号,如果不捕捉,默认动作是终止进程。
输出结果如下:
$ ./catch_user_signal &
[1] 3572
$ kill -USR1 3572
$ received SIGUSR1
$ kill -USR2 3572
$ received SIGUSR2
$ kill 3572
$
[1]+ Terminated ./catch_user_signal
$
默认信号处理动作
当执行一个程序时,所有信号的状态都是系统默认或者忽略。
通常所有信号都被设置为它们的默认动作,而忽略信号的原因一般是执行exec
的进程设置忽略该信号。
exec
函数会将原先设置为要捕捉的信号都更改为默认动作,其他信号的状态不变(因为信号捕捉函数的地址在新程序中不可用)。
例如,对于非作业控制的shell,前台进程与后台进程同处于同一进程组,shell会在执行exec
运行后台进程之前会将中断和退出信号的处理方式设置为忽略。否则,当按下中断字符时,前后台进程都会被终止。
捕捉中断和退出信号的方式(先检测信号是否被忽略,未忽略则捕捉):
void sig_int(int), sig_quit(int);
if (signal(SIGINT, SIG_IGN) != SIG_IGN)
signal(SIGINT, sig_int);
if(signal(SIGQUIT, SIG_IGN) != SIG_IGN)
signal(SIGQUIT, sig_quit);
由此可见,signal()
函数的缺点,不改变信号的处理方式就不能确定当前的处理方式。
当一个进程调用fork
时,子进程继承其父进程的信号处理方式。因为信号捕捉函数在子进程中是有意义的。
作业控制信号
有6个信号与作业控制相关,分别是:
信号 | 作用 |
---|---|
SIGCHLD | 子进程已停止或终止时产生,用于通知父进程。 |
SIGCONT | 如果进程已停止,收到该信号会继续执行。 |
SIGSTOP | 停止进程,不能被捕捉或忽略,相当于强制停止。 |
SIGTSTP | 交互式停止进程,可通过终端按键(Ctrl+Z)产生。 |
SIGTTIN | 后台进程组成员读控制终端时产生,默认停止该进程,然后作业控制shell通知用户。 |
SIGTTOU | 后台进程组成员写控制终端时产生,默认停止该进程,然后作业控制shell通知用户。 |
除了SIGCHLD
信号外,其他信号一般都有作业控制的交互式shell进程来处理的。
我们可以使用shell的作业控制命令fg
或bg
来将作业转入前台或后台运行,这两个命令同时会发送SIGCONT
信号给该进程组。
例如,在打开vim
之后,按键Ctrl+Z
可以停止vim
进程并将其转入后台,然后执行其他动作,最后可以输入fg %1
(%1
是作业编号)将vim
进程转入前台并发送SIGCONT
信号让其继续运行。
在上面的例子中,vim
进程需要在接收到SIGTSTP
信号停止运行之前重新绘制终端屏幕,在执行fg %1
转入前台并接收到SIGCONT
时也要重新绘制屏幕。
这种情况的处理流程如示例所示:
#include <signal.h>
#include <unistd.h>
#define BUFFSIZE 1024
static void sig_tstp(int signo)
{
// 1. 收到SIGTSTP信号之后,需要将光标移到左下角,重置tty模式
// 2. 解除阻塞SIGTSTP信号
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGTSTP);
sigprocmask(SIG_UNBLOCK, &mask, NULL);
// 3. 恢复SIGTSTP信号为默认动作
signal(SIGTSTP, SIG_DFL);
// 4. 向自己发送SIGTSTP信号,让系统执行默认动作
// 正常情况下不会返回,因为kill会等信号递送完才会返回,SIGTSTP信号递送完进程就会转入后台休眠
kill(getpid(), SIGTSTP);
// 当执行fg %1等命令之后,交互式shell会向该进程发送SIGCONT信号,进程继续执行,才会返回到这里
// 5. 进程恢复之后,重新设置SIGTSTP信号为sig_tstp信号捕捉程序
signal(SIGTSTP, sig_tstp);
// 6. 重新设置tty模式,渲染屏幕
}
int main()
{
int n;
char buf[BUFFSIZE];
// 仅在支持作业控制的shell中捕捉SIGTSTP信号
// 如何判断是否支持作业控制呢
// shell进程是由init或者systemd进程拉起的,如果shell不支持作业控制,这两个进程会将SIGTSTP的动作设置为忽略,否则为默认
if (signal(SIGTSTP, SIG_IGN) == SIG_DFL)
{
signal(SIGTSTP, sig_tstp);
}
// 其他动作
}
信号名与编号
函数psignal
可以打印与信号编号对应的字符串,类似于perror
函数。
#include <signal.h>
void psignal(int sig, const char *s);
函数strsignal
可以返回与信号编号对应的字符串,类似于strerror
函数。
#include <string.h>
char *strsignal(int sig);
// 返回值:失败返回NULL
示例如下
#include <signal.h>
#include <stdio.h>
#include <string.h>
int main()
{
psignal(SIGINT, "signal");
printf("%s\n", strsignal(SIGINT));
}
// $ ./signo2str
// signal: Interrupt
// Interrupt
- 原文作者:生如夏花
- 原文链接:https://blduan.top/post/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/apue/%E4%BF%A1%E5%8F%B7%E6%A6%82%E8%BF%B0/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。