1、概念
1.1 定义
在 Linux 下,信号是一种进程间通信机制,用于通知进程发生了某种事件。信号可以由内核、其他进程或进程自身发送给目标进程。实现原理是通过向目标进程发送一个信号号码,目标进程在接收到信号后会根据信号的处理方式(信号处理函数或默认处理方式)来做出相应的处理。
1.2 信号产生
- 键盘输入:用户在终端中按下键盘组合键(如Ctrl+C)可以发送信号给前台进程,通常是SIGINT信号。
- 系统调用或库函数:可以使用kill系统调用或者raise库函数来向指定进程发送信号。
- 硬件异常:硬件故障或异常(如内存访问错误)可能会导致操作系统向进程发送相应的信号(如SIGSEGV)。
1.3 信号状态
信号在Linux系统中有三个状态:未决状态(Pending)、递送状态(Delivered)和处理状态(Handling)。
-
未决状态(Pending):表示信号已经产生,但尚未被进程处理。信号处于未决状态时,可以被阻塞或者等待进程处理。
-
递送状态(Delivered):表示信号已经被递送给进程,但尚未开始处理。在进程接收到信号后,信号会从未决状态转换为递送状态。
-
处理状态(Handling):表示进程正在处理信号,执行信号处理函数。处理状态是信号被处理的最终状态,处理完成后信号会从处理状态转换为完成状态。
阻塞是指进程对某个信号进行了阻塞,即暂时屏蔽了该信号的递送。当信号被阻塞时,即使信号产生了,也不会立即递送给进程,而是保持在未决状态,直到该信号被解除阻塞为止。
1.4 信号处理
在Linux系统中,每个进程都有一个信号处理表(signal table),用于存储对每种信号的处理方式。信号处理方式可以是忽略信号、执行默认操作、执行用户自定义的信号处理函数等。
1.5 小结
信号从产生到处理的状态转换过程如下:
- 信号产生:信号由外部事件(如键盘输入、系统调用、硬件异常等)产生。
- 信号未决:信号被发送给进程,处于未决状态,等待进程处理。
- 信号递送:进程接收到信号,信号从未决状态转换为递送状态。
- 信号处理:进程执行信号处理函数,处理信号的具体操作。
- 信号处理完成:信号处理函数执行完毕,信号处理完成。
2、信号查看与分类
使用kill -l可以查看当前系统的信号:
信号分类汇总如下,不同系统可能稍有差别:
信号 | 数值 | 描述 | 类型 |
---|---|---|---|
SIGHUP | 1 | 终端挂起或控制进程终止 | 标准信号 |
SIGINT | 2 | 中断进程(Ctrl+c) | 标准信号 |
SIGQUIT | 3 | 退出进程 | 标准信号 |
SIGILL | 4 | 非法指令 | 标准信号 |
SIGTRAP | 5 | 跟踪/断点捕获 | 标准信号 |
SIGABRT | 6 | 异常终止 | 标准信号 |
SIGBUS | 7 | 总线错误 | 标准信号 |
SIGFPE | 8 | 浮点异常 | 标准信号 |
SIGKILL | 9 | 强制终止 | 标准信号 |
SIGUSR1 | 10 | 用户定义信号 | 标准信号 |
SIGSEGV | 11 | 无效内存引用 | 标准信号 |
SIGUSR2 | 12 | 用户定义信号 | 标准信号 |
SIGPIPE | 13 | 管道破裂 | 标准信号 |
SIGALRM | 14 | 定时器到期 | 标准信号 |
SIGTERM | 15 | 终止请求 | 标准信号 |
SIGSTKFLT | 16 | 协处理器栈错误 | 实时信号 |
SIGCHLD | 17 | 子进程状态发生改变 | 标准信号 |
SIGCONT | 18 | 继续(使暂停的进程继续执行) | 标准信号 |
SIGSTOP | 19 | 停止进程 | 标准信号 |
SIGTSTP | 20 | 终端停止(通常由 Ctrl+Z 产生) | 标准信号 |
SIGTTIN | 21 | 后台进程尝试读取控制终端 | 标准信号 |
SIGTTOU | 22 | 后台进程尝试写入控制终端 | 标准信号 |
SIGURG | 23 | 紧急情况 | 标准信号 |
SIGXCPU | 24 | 超过 CPU 时间限制 | 标准信号 |
SIGXFSZ | 25 | 超过文件大小限制 | 标准信号 |
SIGVTALRM | 26 | 虚拟定时器到期 | 标准信号 |
SIGPROF | 27 | 定时器到期 | 标准信号 |
SIGWINCH | 28 | 窗口大小调整 | 实时信号 |
SIGIO | 29 | I/O 事件 | 实时信号 |
SIGPWR | 30 | 电源故障 | 实时信号 |
SIGSYS | 31 | 非法系统调用 | 标准信号 |
SIGRTMIN | 34 | 实时信号的起始编号 | 实时信号 |
SIGRTMIN+1 | 35-63 | 实时信号 | 实时信号 |
SIGRTMAX | 64 | 实时信号的结束编号 | 实时信号 |
3、信号编程
3.1 常用接口介绍
3.1.1 signal函数
设置信号处理函数,当收到指定信号时调用该处理函数
sighandler_t signal(int signum, sighandler_t handler);
- 入参:signum - 信号编号,handler - 信号处理函数
- 返回值:成功时返回之前的信号处理函数指针,失败时返回 SIG_ERR
3.1.2 sigaction函数
设置信号处理函数及信号处理选项
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
- 入参:signum - 信号编号,act - 新的信号处理方式,oldact - 之前的信号处理方式
- 返回值:成功时返回0,失败时返回-1
struct sigaction {
void (*sa_handler)(int); // 信号处理函数的指针
void (*sa_sigaction)(int, siginfo_t *, void *); // 信号处理函数的指针,支持传递更多的信号信息
sigset_t sa_mask; // 在处理当前信号时需要屏蔽的信号集合
int sa_flags; // 信号处理的标志,如SA_RESTART等
};
3.1.3 kill函数
向指定进程发送信号。
int kill(pid_t pid, int sig);
- 入参:pid - 进程ID,sig - 信号编号
- 返回值:成功时返回0,失败时返回-1
3.1.4 sigprocmask函数
设置当前进程的信号屏蔽集合。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- 入参:how - 操作类型,set - 新的信号屏蔽集合,oldset - 之前的信号屏蔽集合
- 返回值:成功时返回0,失败时返回-1
how常用取值:
SIG_BLOCK
:将指定信号集合中的信号添加到当前进程的信号屏蔽集合中。SIG_UNBLOCK
:将指定信号集合中的信号从当前进程的信号屏蔽集合中移除。SIG_SETMASK
:将当前进程的信号屏蔽集合替换为指定的信号集合。
3.1.5 sigsuspend函数
挂起进程,直到收到一个信号。
int sigsuspend(const sigset_t *mask);
- 入参:mask - 信号屏蔽集合
- 返回值:永远不会返回,直到收到信号
3.1.6 sigismember函数
挂起进程,直到收到一个信号。
int sigismember(const sigset_t *set, int signum);
- 入参:set:指向一个sigset_t类型的信号集合的指针,用于指定要检查的信号集合。signum:要检查的信号编号。
- 返回值:如果指定信号signum包含在信号集合set中,则返回1;否则返回0。
3.1.7 sigemptyset和sigfillset函数
清空/填满信号集合,即将所有信号从信号集合中移除/加入。
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
- 入参:set:指向一个sigset_t类型的信号集合的指针,用于指定要清空的信号集合。
- 返回值:若成功清空信号集合,则返回0;否则返回-1,并设置errno来指示错误原因。
3.1.8 sigaddset和sigdelset函数
向信号集合中添加/删除指定信号。
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
- 入参:set:指向一个sigset_t类型的信号集合的指针,用于指定要添加/删除信号的信号集合。signum:要添加的信号编号。
- 返回值:若成功向信号集合中添加指定信号,则返回0;否则返回-1,并设置errno来指示错误原因。
3.1.9 raise函数
向当前进程发送指定信号。
int raise(int sig);
- 入参:sig:要发送的信号编号。
- 返回值:若成功向当前进程发送指定信号,则返回0;否则返回非零值。
3.1.10 sigwait函数
等待指定信号的到来。
int sigwait(const sigset_t *set, int *sig);
- 入参:set - 等待的信号集合,sig - 接收到的信号编号
- 返回值:成功时返回0,失败时返回错误码
3.2 编程测试
测试代码如下:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int testNum = 1;
void signal_handler(int signum)
{
printf("Signal Set Received signal %d Recv %d tims\n", signum, testNum);
testNum++;
}
void sigaction_handler(int signum)
{
printf("Sigaction Set Received signal %d\n", signum);
}
int main()
{
struct sigaction sa;
sigset_t current_mask;
sigset_t newset, oldset;
// sigprocmask获取当前进程的信号屏蔽集合
if (sigprocmask(SIG_BLOCK, NULL, ¤t_mask) == -1)
{
perror("sigprocmask");
return 1;
}
// signal设置SIGINT Ctrl+c信号处理函数
signal(SIGINT, signal_handler);
// sigaction设置SIGFPE信号处理函数
sa.sa_handler = sigaction_handler;
// 检查当前信号屏蔽集合,如果包含SIGFPE信号,则清除该信号
if (sigismember(¤t_mask, SIGFPE))
{
sigemptyset(&sa.sa_mask);
}
else
{
// 否则,将当前信号屏蔽集合设置为sa_mask
sa.sa_mask = current_mask;
}
// 设置标志为0,表示无特殊标志
sa.sa_flags = 0;
// 设置SIGINT信号的处理方式为sa
if (sigaction(SIGFPE, &sa, NULL) == -1)
{
perror("sigaction");
return 1;
}
printf("Waiting for signal...\n");
while(1)
{
if(testNum == 6)
{
testNum++;
printf("Ctrl + C Rev 5 , mask it\n");
sigaddset(&newset, SIGINT);
// 设置新的信号屏蔽集合
sigprocmask(SIG_BLOCK, &newset, &oldset);
sleep(3);
}
if(testNum == 7)
{
// 使用raise产生一次SIGFPE
printf("\nraise Generate SIGFPE\n");
raise(SIGFPE);
testNum++;
while(1);
}
usleep(1);
}
return 0;
}
代码使用了3.1中的大部分接口,使用signal和sigaction设置了SIGINT和SIGFPE的自定义处理方式,其中SIGINT(Ctrl + c)信号获取5次后将信号进行屏蔽,3秒钟后并通过软件异常和raise函数生成两次SIGFPE信号:
此时ctrl + c已经无法停止程序,使用ctrl + z暂停程序,然后使用SIGKILL(9)将程序强制停止,SIGKILL是无法被屏蔽的:
4、总结
本文阐述了信号的一些基本概念,列出了应用编程常用的接口,并编写示例进行简单测试。