信号
1. 信号概念
信号的定义
信号是软中断,它提供了一种处理异步事件的方法。在Linux系统中,每个信号都有一个名字,这些名字都已SIG开头,通过kill -l命令可以查看所有信号。
注意上图:
- 信号的编号从1到64,但是没有32,33,所以信号共有62个
- 前31个信号称为普通信号,后31个称为实时信号
- 使用信号的时候可以直接使用编号,也可以使用这些宏
本文内容仅限于前31个普通信号,且仅讨论用的相对较多的部分信号。
信号的产生
信号的产生有多种方式:
- 终端按键产生信号,如Ctrl+C产生SIGINT信号,Ctrl+\产生SIGQUIT信号
- 硬件异常产生信号,如除0操作、无效的内存引用等
- 进程调用kill函数或终端执行kill命令产生信号
- 当检测到某种软件条件已经发生,将其通知给有关进程时也要产生信号,如alarm超时产生SIGALARM信号
信号的处理
信号的处理一般有三种方式:
- 忽略信号。大多数信号都可采用该方式处理
- 执行系统默认动作。每一种信号都有系统默认动作,而大部分信号的默认动作都是终止进程
- 捕捉信号。此种处理方式为执行用户自定义的处理函数
有两个信号比较特殊:SIGKILL和SIGSTOP,它们既不能被忽略,也不能被捕捉。
可靠信号术语
- 信号递送(Delivery):当一个信号产生时,内核通常在进程表中以某种形式设置一个标志,当对信号采取了该动作时,我们就说向进程递送了一个信号
- 信号未决(Pending):信号从产生到递送之间的时间间隔内,我们称信号是未决的
- 信号阻塞(Block):进程可以选择阻塞某个信号,即屏蔽不接收该信号
注意:
- 如果为进程产生了一个阻塞的信号,且进程对该信号的处理方式为系统默认动作或捕捉,那么该信号将一直保持在未决状态
- 阻塞和忽略是不同的,阻塞是进程没有收到该信号,而忽略是进程收到信号后的一种处理方式
Linux常用信号及默认动作
信号 | 产生条件及功能说明 | 系统默认动作 |
---|---|---|
SIGABRT | 调用abort() | 进程终止 + coredump |
SIGALRM | alarm或settimer超时 | 进程终止 |
SIGCHLD | 子进程终止时,发送该信号给父进程 | 忽略 |
SIGFPE | 算术运行异常,如除0、浮点溢出等 | 进程终止 + coredump |
SIGINT | 命令行终端按Ctrl+C时,发送该信号给所有的前台进程,常用于终止运行失控的进程 | 进程终止 |
SIGQUIT | 命令行终端按Ctrl+\时,发送该信号给所有的前台进程,作用和SIGINT相同,但会产生cordump文件 | 进程终止 + coredump |
SIGTSTP | 命令行终端按Ctrl+Z时,发送该信号给所有的前台进程,用于停止进程 | 停止进程 |
SIGIO | 指示一个异步IO事件 | 进程终止/忽略 |
SIGKILL | 不能被捕捉和忽略,只能采取系统默认动作 | 进程终止 |
SIGSTOP | 不能被捕捉和忽略,只能采取系统默认动作 | 进程终止 |
SIGTERM | 由kill函数或kill命令产生,发送给应用程序捕获,reboot命令就用到了该信号 | 进程终止 |
SIGSEGV | 无效内存引用,段错误 | 进程终止 + coredump |
SIGUSR1 | 用户自定义信号,可用于应用程序 | 进程终止 |
SIGUSR2 | 用户自定义信号,可用于应用程序 | 进程终止 |
2. signal函数
signal函数用于给某个信号指定信号处理方式。
#include <signal.h>
typedef void (*sighandler_t)(int);
//成功返回旧的信号处理函数,失败返回SIG_ERR
sighandler_t signal(int signum, sighandler_t handler);
参数说明:
- signum:信号编号
- handler:SIG_IGN(忽略信号)、SIG_DFL(默认动作)或信号处理函数(捕捉信号)
3. 信号集与信号屏蔽字
信号集
信号集sigset_t是一种能表示多个信号的数据类型,signal.h提供了下列5个处理信号集的函数。
#include <signal.h>
//4个函数返回值:成功返回0,失败返回-1
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); //将一个信号signum从信号集set中删除
//若信号signum在信号集set中,则返回1,否则返回0
int sigismember(const sigset_t *set, int signum); //判断信号signum是否在信号集set中
应用程序在使用信号集前,必须要先对该信号集调用一次sigemptyset或sigfillset,之后就可以调用sigaddset或sigdelset在该信号集中增删特定的信号。
信号屏蔽字
- 每个进程都有一个信号屏蔽字,它规定了当前要阻塞而不能递送到该进程的信号集
- 对于每种信号,该屏蔽字中都有一位与之对应,若信号的对应位已设置,则该信号是被阻塞的
- sigprocmask函数可以检测或更改,或同时检测和更改进程的信号屏蔽字
#include <signal.h>
//成功返回0,失败返回-1
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数说明:
- 若oldset不为NULL,则进程的当前信号屏蔽字将通过oldset返回
- 若set不为NULL,则how将指示如何修改当前信号屏蔽字
- 若set为NULL,则忽略参数how,设为0即可
参数how | 说 明 |
---|---|
SIG_BLOCK | 新的信号屏蔽字是其当前信号屏蔽字和set指向信号集的并集,set包含了希望阻塞的附加信号 |
SIG_UNBLOCK | 新的信号屏蔽字是当前信号屏蔽字和set指向信号集补集的交集,set包含了希望解除阻塞的信号 |
SIG_SETMASK | 新的信号屏蔽字是set指向的信号集 |
4. sigaction函数
sigaction用于获取某个信号的现有handler,或者为其设置新的handler,或者二者都有,它改善了signal要想获得当前handler,必须先设置新的handler的缺陷。
APUE推荐用sigaction代替signal,但这个函数用起来比signal麻烦的多,目前实际工程中还是以使用signal居多。
#include <signal.h>
struct sigaction
{
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
//成功返回0,失败返回-1
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数说明:
- signum:信号编号
- act:若act不为NULL,则设置新的handler
- oldact:若oldact不为NULL,则返回旧的handler
当act不为NULL时:
- act->sa_handler应设为新的信号处理函数
- act->sa_mask表示一个信号集,用于指定想要阻塞的信号
- 在调用信号处理函数前,内核会自动将act->sa_mask加入信号屏蔽字;当从信号处理函数返回时,再将信号屏蔽字恢复为原先的值
5. kill和raise函数
kill函数将信号发送给进程或进程组,raise函数用于进程向自身发送信号。
#include <signal.h>
//成功返回0,失败返回-1
int kill(pid_t pid, int signum);
int raise(int signum); //raise(signum) <==> kill(getpid(), signum)
kill的pid参数有以下4种不同的情况:
- pid > 0:将信号发送给进程ID为pid的进程
- pid = 0:将信号发送给调用进程所属的进程组
- pid < 0:将信号发送给进程组ID等于abs(pid),且调用进程有权限发送的所有进程
- pid = -1:将信号发送给调用进程有权限发送的所有进程
6. alarm函数
使用alarm可以设置一个定时器(闹钟时间),当定时器超时时,会产生SIGALRM信号,如果应用程序忽略或不捕捉该信号,则会采用系统默认动作——终止调用alarm的进程。
#include <unistd.h>
//返回值:0或者以前设置的定时器剩余秒数
unsigned int alarm(unsigned int seconds);
- 每个进程只能有一个闹钟时间,闹钟时间只能以秒为单位
- 如果在调用alarm时,之前设置的闹钟时间还没到,则之前闹钟的剩余时间将作为本次alarm的返回值,并使用新的闹钟时间重新计时
- 如果在调用alarm时,之前设置的闹钟时间还没到,且本次调用的seconds为0,则取消之前的闹钟时间,其剩余时间作为本次alarm的返回值
7. pause函数
pause函数使调用进程阻塞直到捕捉到一个信号。
#include <unistd.h>
//返回-1,errno设置为EINTR
int pause(void);
只有执行了一个信号处理函数并从其返回时,pause才返回,在这种情况下,pause返回-1,errno设置为EINTR。
8. 信号可重入函数
信号可重入函数是指在信号处理程序中保证调用安全的函数,也叫做异步信号安全函数,下图列出了所有的信号可重入函数,其中所有的系统调用都是信号可重入函数。
没有列入上图的大多数函数是信号不可重入函数,主要原因有:
- 它们使用静态数据结构
- 它们调用malloc或free
- 它们是标准IO函数
9. 信号的应用
应用程序使用信号的步骤其实很简单:
- 调用signal或sigaction为某个信号指定处理方式
- 如果是捕捉信号,则编写信号处理函数,在函数中实现需要的处理
应用程序使用信号的步骤其实也很复杂:
- 信号的数量繁多
- 每种信号的处理方式都有3种
- 很多系统函数内部也有对信号的要求,我们在使用信号时可能会在无意间与之冲突,比如下一篇笔记system与信号的深层特性中讲的案例
- 一些信号产生原因来自硬件问题或系统故障,这对于应用程序是不可预知、不可控的,要不要处理这些信号、如何处理都是比较麻烦的事情