信号是 Linux 进程间通信的最古老的方式。信号是软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式 。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
Linux 信号可由如下条件产生:
① 对于前台进程,用户可以通过输人特殊的终端字符来给它发送信号。比如输人 Ctrl+C 通常会给进程发送一个中断信号。
② 系统异常。比如浮点异常和非法内存段访问。
③ 系统状态变化。比如 alarm 定时器到期将引起 SIGALRM 信号。
④ 运行 kill 命令或调用 kill 函数。
概述
内核实现信号捕捉的过程:
发送信号
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
功能:
- 把信号 sig 发送给进程 pid
pid
- pid > 0:信号发送给 PID 为 pid 的进程
- pid = 0:信号发送给本进程组内的其他进程
- pid = -1:信号发送给除 init 外的所有进程
- pid < -1:信号发送给组 ID 为 -pid 的进程组中的所有成员
sig
- 要发送的信号
返回值:
- 成功:返回 0
- 失败:返回 -1,设置 errno,errno 如下:
- EINVAL:无效信号
- EPERM:该进程没有权限发送信号给任何一个目标进程
- ESRCH:目标进程或进程组不存在
注意事项一:(发送空信号)
- Linux定义的信号值都大于 0,如果 sig 取值为0,则 kill 函数不发送任何信号
- 但将 sig 设置为 0 可以用来检测目标进程或进程组是否存在,因为检查工作总是在信号发送之前就执行
- 不过这种检测方式是不可靠的,因为:
- 由于进程 PID 的回绕,可能导致被检测的 PID 不是我们期望的进程的 PID
- 这种检测方法不是原子操作
信号处理方式
#include <signal.h>
typedef void (*__sighandler_t)(int);
功能
- 目标进程收到信号后,执行该信号处理函数。
参数
- 信号的编号,这里可以填数字编号,也可以填信号的宏定义
注意事项
- 信号处理函数应该是可重入的,否则很容易引发一些竞态条件,所以在信号处理函数中严禁调用一些不安全的函数
Linux 信号
掌握下面列出的信号即可,其它的了解。
信号 | 信号值 | 默认行为 | 含义 |
---|---|---|---|
SIGHUP | 1 | Term | 控制终端挂起 |
SIGINT | 2 | Term | 键盘输入以中断进程(Ctrl+C) |
SIGQUIT | 3 | Core | 键盘输入使进程退出(Ctrl+) |
SIGKILL | 9 | Term | 终止一个进程,该信号不可被捕获或者忽略 |
SIGSEGV | 11 | Core | 非法内存段引用 |
SIGPIPE | 13 | Term | 往读端被关闭的管道或者 socket 连接中写数据 |
SIGALRM | 14 | Term | 由 alarm 或 setitimer 设置的实时闹钟超时引起 |
SIGTERM | 15 | Term | 终止进程,kill 命令默认发送的信号就是 SIGTERM |
SIGCHLD | 20,17,18 | Ign | 子进程状态发生变化(退出或者暂停) |
SIGURG | 23 | Ign | socket 连接上接收到紧急数据 |
- Term
缺省动作是终止进程。 - Core
缺省动作是终止进程并进行内核映像转储(core dump)。当程序在执行的时候,由于编写的失误或未经过充分的测试,程序对系统构成威胁,就可能会导致核心转储。内核映像转储是指将进程数据在内存的映像和进程在内核结构中的部分内容以一定格式转储到文件系统,以作为调试之用,并且进程退出执行。 - Ign
缺省动作是忽略此信号,将该信号丢弃,不做处理。
signal 系统调用
SIGKILL 和 STGSTOP 不能被捕捉,不能被忽略。
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int sig, sighandler_t handler);
功能:
- 为一个信号设置处理函数
参数
- sig:要捕获的信号
- handler:信号 sig 的处理函数,有 3 种情况:
- SIG_IGN:忽略 sig 所指的信号
- SIG_DFL:使用 sig 信号默认的行为
- 自定义的信号处理函数,信号的编号为这个自定义函数的参数(这个函数是内核调用)
返回值(一般不关心)
- 成功:返回上一次注册的信号处理函数的地址,第一次调用返回 NULL
- 失败:返回 SIG_ERR,设置错误号
案例:
void myalarm(int num) {
printf("捕捉到了信号的编号是:%d\n", num);
}
// 过3秒后,每隔2秒定时一次
int main()
{
// 注册信号捕捉
// signal(SIGALRM, SIG_IGN);
// signal(SIGALRM, SIG_IGN);
signal(SIGALRM, myalarm);
struct itimerval new_val;
// 设置间隔的时间
new_val.it_interval.tv_sec = 2;
new_val.it_interval.tv_usec = 0;
// 设置延迟的时间,3秒后开始第一次定时
new_val.it_value.tv_sec = 3;
new_val.it_value.tv_usec = 0;
// 非阻塞
int ret = setitimer(ITIMER_REAL, &new_val, NULL);
printf("定时器开始....\n");
if(ret == -1) {
perror("setitimer");
exit(0);
}
getchar(); // 将代码阻塞在这里,方便观察输出
return 0;
}
/* 输出:
zengyq@zengyq:~/牛客C++学习$ ./a.out
定时器开始....
捕捉到了信号的编号是:14
捕捉到了信号的编号是:14
捕捉到了信号的编号是:14
捕捉到了信号的编号是:14
*/
sigaction 系统调用
#include <signal.h>
int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact);
功能
- 为一个信号设置处理函数(更健壮)
参数
- sig:要捕获的信号
- act:信号的处理方式
- oldact:输出信号先前的处理方式(一般不是使用,传递 NULL)
返回值
- 成功: 0
- 失败:-1,并设置 errno
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t*, void*); // 不常用
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void); // 已弃用,传NULL即可
}
sa_handler、sa_sigaction
- 信号处理函数指针,和 signal() 里面的函数指针用法一样,应根据情况给 sa_handler、sa_sigaction 两者之一赋值。
sa_mask
- 信号掩码(信号阻塞集),指定哪些信号不能发送给本进程,临时屏蔽指定的信号。
- 每个进程都有一个阻塞集,创建子进程时子进程将继承父进程的阻塞集。
- 所谓阻塞并不是禁止传送信号, 而是暂缓信号的传送。
sa_flags
- 设置程序收到信号时的行为,通常设为 0 ,表示使用 void (*sa_handler)(int); 进行信号处理。
sa_flags 选项 | 含义 |
---|---|
SA_NOCLDSTOP | 如果 sigaction 的 sig 参数是 SIGCHLD,则设置该标志表示子进程暂停时不生成 SIGCHLD 信号 |
SA_NOCLDWAIT | 如果 sigaction 的 sig 参数是 SIGCHLD,则设置该标志表示子进程结束时不产生僵尸进程 |
SA _SIGINFO | 使用 sa_sigaction 作为信号处理函数(而不是默认的 sa_handler),它给进程提供更多相关的信息 |
SA_ONSTACK | 调用由 sigaltstack 函数设置的可选信号栈上的信号处理函数 |
SA_NODEFER | 当接收到信号并进入其信号处理函数时,不屏蔽该信号。默认情况下,我们期望进程在处理一个信号时不再接收到同种信号,否则将引起一些竞态条件 |
SA_RESETHAND | 信号处理函数执行完以后,恢复信号的默认处理方式 |
SA_INTERRUPT | 中断系统调用 |
SA_RESTART | 重新调用被该信号终止的系统调用 |
SA_NOMASK | 同 SA_NODEFER |
SA_ONESHOT | 同 SA_RESETHAND |
SA_STACK | 同 SA_ONSTACK |
信号集
许多信号相关的系统调用都需要能表示一组不同的信号。多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为 sigset_t。
在 PCB 有两个非常重要的信号集。一个称之为“阻塞信号集”,另一个称之为”未决信号集“。这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对 PCB 中的这两个信号集进行修改。
信号的”未决”是一种状态。指的是从信号的产生到信号被处理前的这一段时间。
信号的“阻塞”是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。
信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。
信号集函数
Linux 使用数据结构 sigset_t 来表示一组信号,其实际上是一个长整型数组,数组的每个元素表示一个信号。Linux 提供了如下一组函数来设置、修改、删除和查询信号集:
#include <signal.h>
int sigemptyset(sigset_t *_set); // 清空信号集(set是需要操作的信号集,传出参数)
int sigfillset()(sigset_t *_set); // 在信号集中设置所有信号
int sigaddset()(sigset_t *_set, int _signo); // 将信号_signo添加至信号集中
int sigdelset()(sigset_t *_set, int _signo); // 将信号_signo从信号集中删除
int sigismember()(const sigset_t *_set, int _signo); // 测试_signo是否在信号集中
案例:
int main()
{
// 创建一个信号集
sigset_t set;
// 清空信号集的内容
sigemptyset(&set);
// 判断 SIGINT 是否在信号集中
int ret = sigismember(&set, SIGINT);
if(ret == 0) {
printf("SIGINT 不阻塞\n");
} else if(ret == 1) {
printf("SIGINT 阻塞\n");
}
// 添加几个信号到信号集中
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
// 再次判断 SIGINT 是否在信号集中
ret = sigismember(&set, SIGINT);
if(ret == 0) {
printf("SIGINT 不阻塞\n");
} else if(ret == 1) {
printf("SIGINT 阻塞\n");
}
// 从信号集中删除一个信号
sigdelset(&set, SIGQUIT);
// 再次判断 SIGQUIT 是否在信号集中
ret = sigismember(&set, SIGQUIT);
if(ret == 0) {
printf("SIGINT 不阻塞\n");
} else if(ret == 1) {
printf("SIGINT 阻塞\n");
}
return 0;
}
/*
输出:
zengyq@zengyq:~/牛客C++学习$ ./a.out
SIGINT 不阻塞
SIGINT 阻塞
SIGINT 不阻塞
*/
进程信号掩码
前文提到,我们可以利用 sigaction 结构体的 sa_mask 成员来设置进程的信号掩码。此外,如下函数也可以用于设置或查看进程的信号掩码:
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
功能
- 将自定义信号集中的数据设置到内核(设置阻塞、解除阻塞、替换)
参数
how:如何对内核阻塞信号集进行处理
- SIG_BLOCK:将用户设置的阻塞信号集添加到内核
- SIG_UNBLOCK:根据用户设置的数据,对内核中的数据进行解除阻塞
- SIG_SETMASK:覆盖内核中原来的值
set
- 指定新的信号掩码
- 如果 set 为 NULL,则进程信号掩码不变,此时我们仍然可以利用 oset 参数来获得进程当前的信号掩码
oset:已经初始化好的用户自定义的信号集
oldset:保存设置之前的内核中的阻塞信号集的状态,可以是NULL
返回值
- 成功: 0
- 失败:-1,并设置 errno
被挂起的信号
设置进程信号掩码后,被屏蔽的信号将不能被进程接收。如果给进程发送一个被屏蔽的信号,则操作系统将该信号设置为进程的一个被挂起的信号。如果我们取消对被挂起信号的屏蔽,则它能立即被进程接收到。
如下函数可以获得进程当前被挂起的信号集:
#include <signal.h>
int sigpending(sigset_t *set);
功能
- 获取内核中的未决信号集
参数
- set:保存的是内核中的未决信号集中的信息(传出参数)
返回值
- 成功: 0
- 失败: -1,并设置 errno
fork 调用产生的子进程将继承父进程的信号掩码,但具有一个空的挂起(未决)信号集。
评论区