Linux 进程信号
目录
一、了解信号
1、概念
2、前台进程和后台进程
3、信号的处理
4、硬件层面
5、信号与我们的代码是异步的
二、信号的产生
1、产生的方式
2、键盘组合键
3、kill命令
4、系统调用
man kill
man raise
man abort
5、异常软件条件
(1)异常产生信号
(2)软条件——闹钟
(3)core dump
三、信号保存
1、信号其他的相关概念
2、在内核中表示
3、sigset_t
4、信号集操作函数
(1)sigprocmask
(2)sigpending
我们进程通信讲的信号量和信号没有任何关系
一、了解信号
1、概念
我们举个例子了解一下信号
在我们的生活中处处可见信号,就比如我们网购了一件商品,这个快递在运输的过程,我们已经能够知道这个商品或被用来做什么,因为我们能识别快递
当快递送到了,而我们正在打游戏还有五分钟结束游戏,我们此时不会立即去取快递,而是打完这把游戏再去拿快递,也就是说拿快递这个过程不是立即执行的,我们可以理解成在合适的时间去取
在我们收到快递到了这个信息和拿到这个快递,中间会存在时间窗口。当我们收到这个快递,我们会有三种执行方式:1、默认动作(拆开快递直接使用),2、忽略动作(取到快递直接放一边) 3、自定义动作(我们把这个快递送给别人)。
快递运输到取快递这个过程对我们来说是异步的。
异步的理解:
异步是指进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率。
异步就相当于当客户端发送给服务端请求时,在等待服务端响应的时候,客户端可以做其他的事情,这样节约了时间,提高了效率。
在我们的生活中也处处存在信号:红绿灯,下课铃声等等....
我们怎么认识这些信号呢? 从小有人教过我们。 我们不仅要识别信号,还要直到信号的处理方法,比如红灯停绿灯行。但信号产生了,我们可能并不立即处理这个信号,在合适的时候,因为我们可能正在做更重要的事情。 — 所以,信号产生后一直到信号处理时,中间一定有一个时间窗口。在这个时间窗口内,我们必须记住信号到来!
OS 中也会有很多信号围绕着信号去展开,所以进程要能够识别非常多的信号。这里只想说明进程能够认识信号,以及信号不管到没到来进程都知道该怎么做。
结论:
- 进程必须能识别并能够处理信号,信号没有产生,也要具备处理信号的能力。
- 进程收到一个具体信号的时候,进程可能并不会立即处理这个信号。
- 一个进程从信号产生到信号被处理,一定有时间窗口,进程会在合适的时候处理信号。
2、前台进程和后台进程
如下图 这就是前台进程,我们使用CTRL + C可以杀掉前台进程(该进程运行时,shell不会接收其他命令了)
我们在可执行程序后面加一个& 表示这个程序放在后台运行,这样 Shell 不必等待进程,结束就可以接受新的命令,启动新的进程,在他运行的时候还可以运行其他指令,并且Ctrl+c已经无法杀掉该进程。
我们要终止这个进程,就要用kill -9信号杀死这个进程。
Crtl+c为什么可以终止前台进程呢?
原理是用户按下 Ctrl+C,这个键盘输入产生一个硬件中断,被 OS 获取,解释成信号,发送给目标前台进程。前台进程因为收到信号,进而引起进程退出。
在Linux中,一次登录中,一个终端,一般会配上一个bash,只允许一个进程为前台进程,运行多个进程是后台进程。
那么既然一开始bash是前台进程,那么为什么使用CTRL+C时候,bash不退出呢?
这当然是因为bash在里面对这个信号做了特殊处理
Ctrl+c本质上被进程解释成收到信号,是2号信号,这个后续给大家验证。我们可以用kill -l来看到所有信号。
在我们系统中总共有62个信号(没有0、32、33信号)
1-31号信号被称为普通信号,34-64被称为实时信号。
不会被立即处理的是普通信号,要立即处理的是实时信号。
- 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在 signal.h 中找到,例如其中有定义 #define SIGINT 2
由此我们可以知道,进程就是你,操作系统就是快递员,信号就是快递
3、信号的处理
信号有三种处理方式,我们在上面也提到过
1、默认动作 2、忽略 3、自定义动作(提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数)
我们在上面写的Ctrl+c实际是给该进程发2号信号。我们接下来写个代码测试一下。
我们先认识一个接口。
man signal
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
这个函数的作用是设置对于signum信号的处理方法,处理方法为handler。handler是一个函数指针。
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void handler(int sign)
{
cout<<"process get a signo:"<<sign<<endl;
}
int main()
{
signal(2,handler);
while(1)
{
cout<<"i am a process:"<<getpid()<<endl;
sleep(1);
}
return 0;
}
运行结果如下:
这里我们Ctrl+c并不能终止该进程,因为Ctrl+c实际上是给进程发2号信号,默认执行方式是终止该进程,但我们用signal修改了该进程对信号的处理动作。
对于这个signal函数,只需要设置一次即可,往后都有效
只有收到了对应的信号,才会调用这个方法
注意:不是所有的信号都可以自定义的。有些信号不能自定义
4、硬件层面
我们会有几个问题思考:1、键盘数据是如何输入给内核的呢? 2、ctrl+c又是如何变成信号的呢?
首先键盘输出数据肯定操作系统先知道,那操作系统是怎么知道键盘上有数据的呢?
在冯诺依曼体系结构中,Linux下一切皆文件,键盘也有自己对应的文件,往键盘输入数据本质就是把输入的数据拷贝到缓冲区上。所以操作系统就知道了所以我们就可以用read,write通过文件的方式把数据读到进程当中。
操作系统怎么知道键盘有数据了?
其实在CPU上有很多的引脚,我们的CPU是直接插到主板上的。而键盘是可以间接的和CPU直接物理上连接到的。虽然CPU不从键盘读数据。但是键盘可以给CPU发送一个硬件中断。一旦键盘写完了数据就会给CPU发送硬件中断给CPU,通知CPU数据就绪,从而让操作系统去完成文件的拷贝。显示器和网卡与键盘的原理是一样的,我们用中断号来区分这些硬件, 假如键盘文件的中断号是10,把10存放到CPU寄存器中,告诉CPU这是键盘,让CPU去执行相应的硬件驱动程序。
在软件层面上,操作系统一启动,就会形成一张,中断向量表。里面放的是方法的地址。这些方法是直接访问外设的方法—主要是磁盘,显示器,键盘 然后最后这个读取键盘的方法,才是将键盘的数据放到缓冲区的方法
一句话总结一下这个过程:
当键盘输入数据,会通过硬件中断发送给CPU,CPU会利用这个中断号,让操作系统直接去通过中断向量表找到键盘的读取办法,通过这个办法让键盘上文件数据拷贝到操作系统的缓冲区
所以键盘这个外设是通过中断来工作的。这个就是硬件中断
而我们前面所说的信号,也是通过一堆数字来进行控制。这两者其实比较相似,但是没有关系。一个是软硬件结合的,一个是纯软件行为。
我们所用的信号,就是用软件的方式,对进程模拟的硬件中断
那ctrl+c又是如何变成信号的呢?
实际上把键盘文件数据拷贝到操作系统缓冲区的时候,操作系统会判断是数据还是控制,如果是控制,比如CTRL+ C会把这个转化为2号信号发送给进程。而不是放到缓冲区中。所以进程就收到了2号信号
5、信号与我们的代码是异步的
信号的产生的和我们自己的代码的运行是异步的
同步就是发生一件事后等这件事发生完了才继续做我们的事情
异步就是这件事发生后我们不管这个事情,继续做我们的事情
信号是进程之间事件异步通知的一种方式,属于软中断
二、信号的产生
1、产生的方式
- 键盘组合键
- kill命令
- 系统调用
- 异常软件条件
产生的信号都是由操作系统给进程发送,因为操作系统是进程的管理者。
2、键盘组合键
我们刚刚讲了Ctrl+c是发送2号信号,再给大家讲两个组合键产生的信号
使用CTRL + \即可捕捉3号信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void handler(int sign)
{
cout<<"process get a signo:"<<sign<<endl;
}
int main()
{
signal(3,handler);
while(1)
{
cout<<"i am a process:"<<getpid()<<endl;
sleep(1);
}
return 0;
}
CTRL + Z是19号信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void handler(int sign)
{
cout<<"process get a signo:"<<sign<<endl;
}
int main()
{
signal(19,handler);
while(1)
{
cout<<"i am a process:"<<getpid()<<endl;
sleep(1);
}
return 0;
}
如下所示,我们似乎会发现,我们上面似乎并没有将19号信号用自定义的方法进行处理
其实这是因为不是所有的信号,都是可以被signal捕捉的,比如19,9号信号
我们可以用下面的代码进行测试。
我们能看到9号信号是无法被捕捉的,这里就不给大家全部演示了。
3、kill命令
我们在上面的实验也证明了kill命令也是可以给进程发信号的。
4、系统调用
man kill
int kill(pid_t pid, int sig);
它的两个参数分别是pid和信号的编号。与命令行中的kill是很相似的
如果成功返回0,失败返回-1
我们可以简单的利用这个系统调用接口实现一个kill命令
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void Usage(string proc)
{
cout<<"Usage:\n\t"<<proc<<"signum pid\n\n";
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
Usage(argv[0]);
return 1;
}
int signum=stoi(argv[1]);
pid_t pid=stoi(argv[2]);
int n = kill(pid,signum);
if(n < 0)
{
perror("kill");
return 2;
}
return 0;
}
运行结果如下:
man raise
int raise(int sig);
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void myhandler(int signo)
{
cout << "process get a signal: " << signo << endl;
exit(1);
}
int main()
{
signal(2, myhandler);
int cnt = 5;
while(true)
{
cout << "I am a process, pid: " << getpid() << endl;
sleep(1);
cnt--;
if(cnt == 0) raise(2);
}
return 0;
}
运行结果如下:
这个raise相当于
kill(getpid(), 2);
man abort
void abort(void);
它的作用是引起一个正常的进程直接终止
它相当于给自己发送一个6号信号
我们先用下面代码进行测试
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void myhandler(int signo)
{
cout << "process get a signal: " << signo << endl;
exit(1);
}
int main()
{
signal(6,myhandler);
while(true)
{
cout << "I am a process, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
5、异常软件条件
(1)异常产生信号
我们先来看一段代码
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <signal.h>
using namespace std;
int main()
{
cout<<"div before"<<endl;
sleep(3);
int a=10;
a/=0;
cout<<"div after"<<endl;
return 0;
}
运行结果如下:
我们看到这个Floating point exception实际上就是信号,我们kill -l查看到是8号信号 我们也可以查看7号手册加以验证
我们用signal捕捉8号信号看运行是什么情况
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void myhandler(int signo)
{
cout << "process get a signal: " << signo << endl;
}
int main()
{
signal(8,myhandler);
cout<<"div before"<<endl;
sleep(3);
int a=10;
a/=0;
cout<<"div after"<<endl;
return 0;
}
运行结果如下:
这个我们发现是除0错误导致的,我们在看看野指针造成什么情况。
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void myhandler(int signo)
{
cout << "process get a signal: " << signo << endl;
}
int main()
{
cout<<"point error before"<<endl;
int *p=nullptr;
*p=10;
cout<<"point error after"<<endl;
}
运行结果如下:
我们查看这是11号信号
我们捕捉一下11号信号看看
void myhandler(int signo)
{
cout << "process get a signal: " << signo << endl;
}
int main()
{
signal(11,myhandler);
cout<<"point error before"<<endl;
int *p=nullptr;
*p=10;
cout<<"point error after"<<endl;
}
运行结果显示也是一直打印
这里为什么除0和野指针问题会导致进程崩溃呢?
这里我们曾经讲过在CPU上有一个eip/pc寄存器来记录当前执行的是哪一行代码
这里我们再讲一个,其实在CPU内部还一个状态寄存器,在它的签名有一个溢出标志位,当我们的代码除0,可以理解成在除一个极限小的数字,所以溢出了,在CPU状态寄存器的溢出标志位设置为1,意味着CPU在执行当前进程出异常了,又因为操作系统是硬件的管理者,得知CPU在执行当前进程出异常,所以操作系统给当前进程发送信号,但这个信号被我们捕捉了,它本来默认是终止,但这个异常没有解决,操作系统就一直发信号,所以才会出现一直打印的情况。 这里这个行为并不影响其他进程,我们之前讲过在CPU里的寄存器是进程的上下文,修改CPU内部的状态只影响了自己。这也更好的验证了进程的独立性。
野指针的问题
如下图所示,在CPU里面有一个内存管理单元,因为直接查页表太慢了,所以有一个MMU硬件来进行查表。一旦异常,也就是地址转化失败了。虚拟到物理转化失败了。在CPU内还有一个寄存器,一旦转化失败了。它会把转化失败的虚拟地址放在这里,CPU异常操作系统就知道了,给进程发信号终止该进程。后面的原理是一样的。
这里是对CPU的硬件不同的报错,操作系统能检测并分辨出是除0错误还是野指针问题。
那么异常只会由硬件产生吗?
比如我们之前的管道,如果一开始读写端都打开,但是我们突然关闭了读端。那么写端进程就会被杀掉。会收到一个SIGPIPE(13)号信号。这就是一种软件异常。
也有的异常,操作系统只是会返回值出错的形式进行处理
(2)软条件——闹钟
man alarm
unsigned int alarm(unsigned int seconds);
alarm 系统调用用于设置一个定时器,当定时器计时器达到指定的时间时,内核会发送一个 SIGALRM 信号(14号信号)给调用进程。这可以用于实现定时器功能,例如在一定时间间隔内执行某个特定的操作或执行定时任务
seconds 参数表示定时器的秒数。如果 seconds 参数为非零值,表示设置定时器,在指定秒数后会发送 SIGALRM 信号给进程。如果 seconds 参数为零,则表示取消之前设置的定时器。
返回值是剩余的未完成的定时器秒数。如果之前有一个定时器已经设置,调用 alarm 会取消之前的定时器,并返回剩余的秒数。如果没有之前的定时器,或者之前的定时器已经到期,返回值为 0。
我们来看这段代码
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <signal.h>
using namespace std;
int main()
{
int n=alarm(5);
while(1)
{
cout<<"proc is running"<<endl;
sleep(1);
}
return 0;
}
运行结果如下:
我们尝试捕捉一下14号信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void myhandler(int signo)
{
cout << "process get a signal: " << signo << endl;
}
int main()
{
signal(14,myhandler);
int n=alarm(5);
while(1)
{
cout<<"proc is running"<<endl;
sleep(1);
}
return 0;
}
运行结果如下
因为闹钟只会响一次,所以我们只捕捉了一次。
如果我们想让它每隔3秒响一次
我们再来看代码
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void myhandler(int signo)
{
cout << "process get a signal: " << signo << endl;
int n=alarm(3);
}
int main()
{
signal(14,myhandler);
int n=alarm(3);
while(1)
{
cout<<"proc is running"<<endl;
sleep(1);
}
return 0;
}
关于它的返回值,我们做一下试验
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void myhandler(int signo)
{
cout << "process get a signal: " << signo << endl;
int n=alarm(3);
cout<<"剩余时间:"<<n<<endl;
}
int main()
{
signal(14,myhandler);
int n=alarm(50);
while(1)
{
cout<<"proc is running,pid:"<<getpid()<<endl;
sleep(1);
}
return 0;
}
返回值返回的是上一个闹钟的剩余时间。
(3)core dump
先解释一下什么是 Core Dump,当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是 core,这叫做 Core Dump。
进程异常终止通常是因为有 Bug,比如非法内存访问导致段错误,事后可以用调试器检查 core 文件以查清错误原因,这叫做 Post-mortem Debug(事后调试)。一个进程允许产生多大的 core 文件取决于进程的 Resource Limit(这个信息保存在 PCB 中)。默认是不允许产生 core 文件的,因为 core 文件中可能包含用户密码等敏感信息,不安全。
在开发调试阶段可以用 ulimit 命令改变这个限制,允许产生 core 文件。 首先用 ulimit 命令改变 Shell 进程的 Resource Limit,允许 core 文件最大为 1024K: $ ulimit -c 1024
我们可以先用ulimit -a查看
我们用上面提到的命令使其产生core文件。大小为1024
设置 core file size,kill -8/11 后,发现报错信息中多了一个(core dumped),且 ll 还发现多了一个 core 文件
ulimit 命令改变了 Shell 进程的 Resource Limit,test 进程的 PCB 由 Shell 进程复制而来,所以也具有和 Shell 进程相同的 Resource Limit 值,这样就可以产生 Core Dump 了。
前面讲进程等待的时候说过一个概念,父进程中 waitpid 可以获取子进程的退出信息,其中 status 中,低 7 位表示进程退出时的终止信号,次低 8 位表示进程退出时的退出码,而低 8 位中的最后 1 位还没有讲,它表示进程是否 core dump,core dump 是一个标志位。
当一个进程被异常退出时,退出码没有意义,我们不仅想知道它的退出信号,更想知道的是它在代码的哪一行触发的信号。因为云服务器默认看不到现象,如果是虚拟机的话就可以看到。所以为了让云服务器能够看到,我们就需要设置一下,ulimit -a 查看系统资源,其中 ulimit -c 1024 就设置好了 core file size。
在上面运行报错后,有一个(core dumped),它叫做核心转储。当一个进程崩溃时,OS 会将进程运行时的核心数据 dump 到磁盘上,方便用户进行调试,一旦发生核心转储,core dump 标志位就会被设置 1,否则就是 0。
一般而言,线上环境的核心转储是被关闭的。因为程序每崩溃一次就会 dump 一次,而这一个 core 文件有 56 万多个字节,还不说这个文件不大。如果线上环境的核心转储是打开的,那么在公司项目中有几千台机器,那肯定是自动运行的,此时如果存在大量错误,一运行就 dump,一 dump 就运行,那么过了一晚,服务器肯定都登不上了,原因就是磁盘已经被大量的 core 文件占用了。
三、信号保存
1、信号其他的相关概念
在操作系统给进程发送信号的时候,实际是给进程的PCB发送。
在task_struct中维护一个int signal,我们不把它当整数,我们可以理解成一个32位的数组,或者是位图,把他当成二进制来看,用0,1来描述信号,用位图来管理信号。
所谓的发信号,本质上是操作系统去修改task_struct的信号位图对应的比特位。
那为什么是操作系统发送信号呢?
因为操作系统是进程的管理者,只有操作系统有资格去修改tast_struct内部的属性。
为什么要信号保存呢?
进程收到信号之后,可能不会立即处理这个信号,会有一个不被处理的时间窗口,所以我们要把信号保存起来。
- 实际执行信号的处理动作(忽略、默认、自定义捕捉)称为信号递达(Delivery)。
- 信号从产生到递达之间的状态,称为信号未决(Pending)。
- 进程可以选择阻塞(Block)某个信号。
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
注意 :阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。我们可以把阻塞理解成已读不回,忽略理解成未读。
2、在内核中表示
实际在 Linux kernel 的 task_struct 中还包含了一些信号相关字段,如下面这个信号在内核中的示意图:这个图应该横着来看:
SIGHUP(1),没有收到 pending,也没有收到 block,所以默认处理是 SIG_DFL。
SIG_INT(2),收到 pending,因为也收到了 block,所以不会处理 SIG_IGN。
SIGQUIT(3),没有收到 pending,收到了 block,如果没有收到对应的信号,照样可以阻塞信号,所以阻塞更准备的理解是它是一种状态;
信号的自定义捕捉方法是用户提供的,是在用户权限下对应的方法。下面学习信号的操作都是围绕着这三个表来展开。
- pending(未决):它是一个无符号整型的位图,比特位的位置代表信号的编号,比特位的内容 0 1 代表是否收到信号,OS 发送信号本质是修改 task_struct ➡ pending 位图的内容。
- handler(递达):它是一个函数指针数组,它是用信号的编号,作为 handler 数组的索引,找到该信号编号对应的信号处理方式,然后执行对应的方法。
- block(阻塞):它是一个无符号整型的位图,比特位的位置代表信号的编号,比特位的内容 0 1 代表是否阻塞该信号。
- 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP 信号未阻塞也未产生过,当它递达时执行默认处理动作。
- SIGINT 信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
- SIGQUIT 信号未产生过,一旦产生 SIGQUIT 信号将被阻塞,它的处理动作是用户自定义函数 sighandler。如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?。Linux是这样实现的:常规信号在递达之前产生多次只计一次,如果存在一个信号多次的情况,在递达的时候再把block对应的信号设置为1,也就是阻塞该信号,当递达结束后,再解除该信号的阻塞。而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号
3、sigset_t
可以理解为了能让我们更好的对上面的三张表操作,OS 给我们提供了一种系统级别 sigset_t 类型这个类型 OS 内部的当然也有定义,我们可以使用这个数据类型在用户空间和内核交互,此时就一定需要系统接口。
未决和阻塞标志可以用相同的数据类型 sigset_t 来存储,sigset_t 称为信号集
sigset_t 定义的变量 set 当然是在栈上开辟空间,那么这个栈就是用户栈,实际上我们在进程地址空间中谈的代码段、数据段、堆区、内存映射段、栈区、命令行参数、环境变量都是在用户空间,而将来要把用户空间中的进程信号属性设置到内核,所以除了 sigset_t,一定还需要系统接口。
4、信号集操作函数
当然光有 sigset_t 这个类型还不够,这个类型本身就是一个位图。实际我们不支持或者不建议直接操作 sigset_t,因为不同平台,甚至不同位数的 OS,sigset_t 位图的底层组织结构实现可能是不一样的,所以 OS 提供了一些专门针对 sigset_t 的系统接口,这些接口会先在用户层把信号相关的位图数据处理好。
这些函数是以位图为单位,将位图全部清理或者全部置1等。
#include <signal.h>
int sigemptyset(sigset_t* set);//全部置0
int sigfillset(sigset_t* set);//全部置1
int sigaddset(sigset_t* set, int signo);//指定位置置为1 信号集添加一个信号
int sigdelset(sigset_t* set, int signo);//指定位置置为0 信号集指定信号删除
int sigismember(const sigset_t* set, int signo);//判断特定信号是否已经被设置
- 函数 sigemptyset 初始化 set 所指向的信号集,使其中所有信号的对应 bit 清零,表示该信号集不包含任何有效信号。
- 函数 sigfillset 初始化 set 所指向的信号集,使其中所有信号的对应 bit 置位,表示该信号集的有效信号包括系统支持的所有信号。
注意:在使用 sigset_ t 类型的变量之前,一定要调用 sigemptyset 或 sigfillset 做初始化,使信号集处于确定的状态。初始化 sigset_t 变量之后就可以在调用 sigaddset 和 sigdelset 在该信号集中添加或删除某种有效信号。
这四个函数都是成功返回 0, 出错返回 -1 。 sigismember 是一个布尔函数, 用于判断一个信号集的有效信号中是否包含某种信号, 若包含则返回 1, 不包含则返回 0, 出错返回 -1 。
(1)sigprocmask
调用函数 sigprocmask 可以读取或更改进程的信号屏蔽字(阻塞信号集)。传入一个 set 信号集,设置进程的 block 位图,一般把用户空间定义的信号集变量或对象设置成进程 block 位图,这样的信号集叫做信号屏蔽字(Signal Mask),阻塞信号集也叫做当前进程的信号屏蔽字,这里的屏蔽应该理解为阻塞而不是忽略。
set:输入型参数,由用户层把信号屏蔽字拷贝到内核。
oset:输出型参数,把老的信号屏蔽字返回,方便恢复,不想保存可设置 NULL。
如果 oset 是非空指针,则读取进程的当前信号屏蔽字通过 oset 参数传出。如果 set 是非空指针,则更改进程的信号屏蔽字,参数 how 指示如何更改。如果 oset 和 set 都是非空指针,则先将原来的信号屏蔽字备份到 oset 里,然后根据 set 和 how 参数更改信号屏蔽字。假设当前的信号屏蔽字为 mask,下表说明了 how 参数的可选值。
如果调用 sigprocmask 解除了对当前若干个未决信号的阻塞,则在 sigprocmask 返回前,至少将其中一个信号递达。
返回值
成功返回0,失败返回-1
(2)sigpending
获取当前调用进程的 pending 信号集, 通过 set 参数传出。调用成功则返回 0, 出错则返回 -1 。
set为输出型参数
我们写一下代码来熟悉一下这些接口
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void PrintPending(sigset_t &pending)
{
for(int signo=31;signo>=1;signo--)
{
if(sigismember(&pending,signo))
{
cout<<"1";
}
else
{
cout<<"0";
}
}
cout<<endl;
}
int main()
{
//定义信号集变量
sigset_t bset,oset;
sigset_t pending;
//初始化
sigemptyset(&bset);
sigemptyset(&oset);
sigemptyset(&pending);
//添加想要的屏蔽的信号
sigaddset(&bset,2);
//将该屏蔽的信号设置到block中
sigprocmask(SIG_BLOCK,&bset,&oset);
//重复打印pending信号集
while(1)
{
int n=sigpending(&pending);
if(n<0) continue;
PrintPending(pending);
sleep(1);
}
}
运行结果如下:我们屏蔽了2号信号,当我们按下Ctrl+c,发现pending位图接收到了2号信号,由0->1。
我们想让这个信号过10秒解除,并能递达
我们能看到我们发送2号信号的时候,pending位图上显示,接收到了2号信号,可此时2号信号被屏蔽了,一共循环了10次 解除了屏蔽,2信号递达了并执行默认动作。
当我们捕捉2号信号看看,使其执行自定义动作
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void handler(int signo)
{
cout<<"hello linux"<<endl;
}
void PrintPending(sigset_t &pending)
{
for(int signo=31;signo>=1;signo--)
{
if(sigismember(&pending,signo))
{
cout<<"1";
}
else
{
cout<<"0";
}
}
cout<<endl;
}
int main()
{
signal(2,handler);
//定义信号集变量
sigset_t bset,oset;
sigset_t pending;
//初始化
sigemptyset(&bset);
sigemptyset(&oset);
sigemptyset(&pending);
//添加想要的屏蔽的信号
for(int i=1;i<31;i++)
{
sigaddset(&bset,i);
}
//将该屏蔽的信号设置到block中
sigprocmask(SIG_BLOCK,&bset,&oset);
int count=0;
//重复打印pending信号集
while(1)
{
int n=sigpending(&pending);
if(n<0) continue;
PrintPending(pending);
cout<<"proc id:"<<getpid()<<endl;
if(count++==10)
{
//解除2号信号的屏蔽
sigprocmask(SIG_SETMASK,&oset,nullptr);
cout<<"2号信号解除"<<endl;
}
sleep(1);
}
}
运行结果如下:
信号捕捉执行了自定义动作。