Linux信号
Linux信号基本概念
信号(Signal) 是一种进程间通信(IPC)机制,用于通知进程发生了某种事件。信号是异步的,意味着它可以在任何时候发送给进程,而进程不需要主动等待信号的到来,信号具有以下特点:
- 异步性:信号可以在任何时候发送给进程,进程不需要主动等待。
- 简单性:信号只能传递一个编号,不能携带额外的数据(实时信号除外)。
- 优先级:某些信号(如 SIGKILL 和 SIGSTOP)不能被捕获或忽略。
- 默认行为:每个信号都有一个默认行为,如终止进程、忽略信号或生成核心转储文件。
信号类型
在Linux系统中信号有两种类型,一种是标准信号,一种是实时信号,可以使用kill -l 命令来列出所有信号。如图
其中1到31号为标准信号,具有默认的处理方式,默认处理方式分为Stop(暂停进程),Term(终止进程),Core(终止进程并生成核心转储文件)。不同信号的默认处理方式不同分为stop,core,term,stop就是把进程挂起,可以通过SIGCONT恢复进程,term是终止进程,而core终止之后会进行核心转储,生成一个core文件,利用gdb(Linux调试功能)可以很快定位到出错位置。
而实时信号编号范围从SIGRTMIN到SIGRTMAX,通常为34到64,提供更多的自定义信号。实时信号在设计上更灵活、可靠且功能强大,适用于对实时性和可靠性要求较高的场景,而标准信号则适用于一般用途。
信号的发送与接收
在进程中可以使用 kill() 系统调用向其他进程发送信号,使用 raise() 系统调用向当前进程发送信号,或者是在shell中使用kill命令来发送信号。
#include<iostream>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
int main()
{
pid_t pid=getpid();
int i=0;
while(true)
{
i++;
std::cout<<"i = "<<i<<std::endl;
if(i==5)
{
kill(pid,SIGKILL);//发送9号信号
}
}
std::cout<<"-------------------------------"<<std::endl;
}
可以看到进程在i=5的时候接收到了9号信号,执行9号信号使进程终止,从而没有出现分割线。
信号处理机制
默认信号处理
1到31号为标准信号,具有默认的处理方式,默认处理方式分为Stop(暂停进程),Term(终止进程),Core(终止进程并生成核心转储文件)
自定义信号处理
我们平时常用的CTRL+C用于终止进程实际上就是向进程发送了2号信号SIGINT,从而执行2号信号的默认处理方式,除了默认处理方式还可以利用signal函数捕捉信号并选择自定义处理方式,signum是信号值,handler是函数指针。
signal(int signum, sighandler_t handler);
我们可以使用以下代码去验证一下CTRL+C是不是发送的2号信号,这里可以看到当我们用CTRL+C终止进程,进程并未终止,而是去执行了自定义的处理方式,原因就是我们捕捉到了2号信号。这种情况下我们可以使用kill -9 +进程pid 向进程发送9号信号,9号,19号信号是不允许被捕捉,阻塞和忽略的。
void handle(int n)
{
std::cout<<"this is number 2 "<<std::endl;
}
int main()
{
signal(2,handle);//捕捉2号信号
while(1)
{
std::cout<<"my pid is "<<getpid()<<std::endl;
sleep(1);
}
}
信号阻塞与忽略
信号执行称之为信号递达,信号如果处于产生与递达之间称为未决(还未执行),信号可以被阻塞(block),信号如果被阻塞,除非进程主动解除阻塞,否则将一直不递达,例如如果2号信号被阻塞,那么所有给该进程的2号信号都将不被递达,一直处于未决。(信号会在进程空闲时被执行,如果进程忙碌将短暂保存)
进程描述符中有两个信号集和一个处理方式表,block(阻塞信号集)和pending(挂起信号集)都是32位的位图结构分别表示信号是否阻塞,是否未决,block位图中1表示阻塞,0表示未阻塞,pending位图中的0表示没有信号未决,1表示有信号未决,而下标则表示是几号信号,下标为0表示1号信号,而下标为30则表示31号信号,下标31未用,handler表则是一个函数指针数组,之前的捕捉信号然后自定义处理本质上就是修改函数指针数组中对应下标(信号)的值。
因为这些信号集都处于内核中,所以Linux为我们提供了一系列的系统调用接口,为我们封装了一个新的数据类型sigset_t,本质上就是位图结构。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how:操作类型:
SIG_BLOCK:将 set 中的信号添加到阻塞信号集中。
SIG_UNBLOCK:将 set 中的信号从阻塞信号集中移除。
SIG_SETMASK:将阻塞信号集设置为 set。
set:要操作的信号集。
oldset:保存旧的阻塞信号集。
int sigemptyset(sigset_t *set); // 清空信号集
int sigfillset(sigset_t *set); // 填充所有信号
int sigaddset(sigset_t *set, int signo); // 添加信号到信号集
int sigdelset(sigset_t *set, int signo); // 从信号集中删除信号,signo为信号编号,而非位图下标
int sigismember(const sigset_t *set, int signo); // 检查信号是否在信号集中
int sigpending(sigset_t *set);// 获取挂起的信号集
当我使用一个终端跑该代码,而另一个终端使用kill命令向该进程发送3号信号,我们可以看到pending信号集中3号信号置1,则表示3号信号未决。但是是否阻塞与是否未决无关,你阻塞了一个信号,只有当进程接收到这个信号才会出现未决。
int main()
{
sigset_t block, oldset, pending;
sigemptyset(&block);
sigaddset(&block, SIGQUIT);//将3号信号加入到阻塞信号集中
sigprocmask(SIG_SETMASK, &block, &oldset);
std::cout << "my pid is" << getpid() << std::endl;
while (true)
{
sigpending(&pending);//获取pending表
for (int i = 1; i <= 31; i++)
{
std::cout << sigismember(&pending, i);
}
std::cout << std::endl;
sleep(2);
}
}
在 Linux 中,信号忽略是指进程对某个信号的处理方式设置为忽略,即当该信号发生时,进程不会采取任何行动。忽略信号可以通过以下方式实现,只需要将handler设置为SIG_IGN,即可忽略信号。
#include <signal.h>
void (*signal(int signo, void (*handler)(int)))(int);
进程退出状态
我们知道我们可以在父进程中利用waitpid()等待子进程退出,从而得到进程退出状态保存在status当中,status是一个16位整型数据,status保存的进程退出状态,其值根据子进程的退出方式(正常终止或被信号终止)而不同,且每个比特位的含义也不同。其中当被信号杀死时第七位将保存core dump标志用于标明有无生成核心转储。
waitpid(pid_t pid, int *status, int options);