●🧑个人主页:你帅你先说.
●📃欢迎点赞👍关注💡收藏💖
●📖既选择了远方,便只顾风雨兼程。
●🤟欢迎大家有问题随时私信我!
●🧐版权:本文由[你帅你先说.]原创,CSDN首发,侵权必究。
📌📌📌为您导航📌📌📌
1.信号概念
信号是进程之间事件异步通知的一种方式,属于软中断。
我们首先要明确的是信号和信号量是完全不同的两个概念,他们没有关系,只是名字很像而已。
信号我们很好理解,就是字面意思。
比如你正在上某节课时,突然一声"TiMi~~",这个时候你就明白有人上号了,这里的"TiMi"就是一种信号。
我们来看看Linux的常见信号。
我们发现,虽然标有64,但不是连续的,31以后直接跳到了34,实际上有62个信号,前31个信号我们叫做普通信号
,后31个我们叫做实时信号
。
2.产生信号
2.1键盘产生信号
Linux系统提供了一个可以修改进程对信号的默认处理动作。
我们用代码来使用一下这个函数
刚开始我们没有发送信号时,函数没有被调用,但我们在键盘上敲出Ctrl C
,系统就接收到了信号。
信号的产生方式其中一种就是通过键盘产生的信号,只能用来终止前台进程。
总结:
进程收到信号的处理方案有三种情况。
1.默认动作—一般是终止自己、暂停等。
2.忽略动作—是一种信号处理的方式,只不过动作就是什么也不干。
3.自定义动作(信号的捕捉)—我们刚刚用signal方法,就是在修改信号的处理动作,由默认动作变成自定义动作。(注意:9号信号不可以被捕捉,即不能被自定义)
在Windows 或 Linux下,进程奔溃的本质是进程收到了对应的信号,然后进程执行信号的默认处理动作(杀掉进程)。
为什么会发送信号?
在操作系统中,软件上面的错误,通常会体现在硬件或其他软件上。当CPU发现硬件被破坏了就会发送信号来终止进程。
当你程序崩溃时,你肯定会收到崩溃的原因,这个崩溃的原因我们之前讲过,是在waitpid里status的低七位存储的。
总而言之,在Linux中,当一个进程正常退出的时候,它的退出码和退出信号都会被设置。当一个进程异常退出时,进程的退出信号会被设置,表明当前进程退出的原因。有的时候,OS会设置退出信息中core dump标志位,并将进程在内存中的数据转储到磁盘中,方便我们后期调试。
2.2进程异常产生信号
我们来看看怎么用status来查看退出信息。
我们知道0是不能做除数的,所以对应8号信号的浮点数错误。
2.3系统调用产生信号
除了我们在命令行上敲
kill 信号 pid
来控制信号外,我们还可以通过系统调用接口kill()
来操控信号。
我们发现这个函数使用起来非常容易,只需要pid和信号就可以执行。下面来看看kill的使用方法。
命令行中我们输入 ./xxxx 信号 pid,所以argc是3。
还有两个系统调用也可以发送信号
void abort(void);//向自身发送6号信号
int raise(int sig);//指定发送某个信号
2.4软件条件产生信号
通过某种软件来触发信号的发送。
例如在系统层面设置定时器,或者某种操作而导致条件不就绪等这样的场景下,触发的信号发送。
我们之前将进程间通信时,当读端不读,还关闭了fd,写端一直在写,最终写进程会收到sigpipe(13),这就是一中典型的软件条件触发的信号发送。
我们再来认识个接口
设置在seconds秒后发送一个SIGALRM信号。
**这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。**当seconds等于0时表示取消闹钟。
前面说了那么多,总结一下就是,产生信号有三种方式:
1.键盘产生信号。
2.进程异常产生信号。
3.通过系统调用产生信号。
4.软件条件产生信号。
学到这里,我们可能还是有困惑,OS系统是如何给进程发送信号的?
实际上是task_struct里定义了用于保存记录是否收到了对应信号的变量,在这里用到了我们之前在文件系统里所讲的位图结构,但接收到信号就把该信号置为1,没收到则为0。所以OS给进程发送信号的本质是向指定进程的task_struct中的信号位图写入bit位。与其说是发送信号,不如说是写入信号。
例如:
00000000000000000000000000001001
这个二进制序列的含义是收到了1号和4号信号。
3.阻塞信号
3.1信号其他相关常见概念
- 实际执行信号的处理动作称为信号递达(自定义捕捉、默认、忽略)
- 信号从产生到递达之间的状态,称为信号未决(本质是这个信号被暂存在task_struct信号位图中,未决)。
- 进程可以选择阻塞 (Block )某个信号(本质是OS系统允许进程暂时屏蔽指定的信号,该信号依旧是未决的,该信号不会被递达,直到解除阻塞才能递达)。
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
- 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
3.2信号在内核中的结构
pending位图实际上就是我们刚刚讲的用来标识是否接收到了信号。(已经收到但是还没有被递达的信号)。
阻塞位图和pending位图类型,代表的是这个信号是否被阻塞。
所以OS对于信号的处理可能是这样的
int isHandler(int signo)
{
//信号被阻塞
if(block & signo)
{
//不做处理
}
else
{
//如果信号被递达,则处理
if(signo & pending)
{
hander_array[signo];
return 0;
}
}
return 1;
}
总结:
一个信号总是能被pending,但是能不能递达取决于是否被block。
3.3 sigset_t
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,
sigset_t称为信号集
,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞
,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态
。阻塞信号集也叫做当前进程的信号屏蔽字
(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
信号集操作函数
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
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。
sigprocmask
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
//函数的本质就是修改block位图
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。
来看段代码
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
void handler(int signo)
{
while(1)
{
printf("get a signo: %d\n", signo);
sleep(1);
}
}
int main()
{
sigset_t iset,oset;
sigemptyset(&iset);
sigemptyset(&oset);
sigaddset(iset, 2);
//sigaddset(iset, 9);9号信号不可被屏蔽!
//设置屏蔽字
sigprocmask(SIG_SETMASK,&iset,&oset);
while(1)
{
printf("hello world!\n");
sleep(1);
}
return 0;
}
此时2号信号对应的Ctrl C键就失效了。
sigpending
#include <signal.h>
sigpending(sigset_t * set);
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。
//不对pending位图修改,只获取进程的pending位图
接下来我们来实现一个功能。
首先屏蔽掉2号信号,不断的获取当前进程的pending位图,并打印显示,手动发送2号信号(信号不会被递达),然后再不断的获取当前进程的pending位图,并打印。
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
void show_pending(sigset_t *set)
{
printf("curr process pending: ");
for(int i = 1; i <= 31; i++)
{
if(sigismember(set, i))
{
printf("1");
}
else
{
printf("0");
}
}
printf("\n");
}
void handler(int signo)
{
printf("%d 号信号被递达了,已经处理完成!\n", signo);
}
int main()
{
signal(2, handler);
sigset_t iset, oset;
sigemptyset(&iset);
sigemptyset(&oset);
sigaddset(&iset, 2);
sigprocmask(SIG_SETMASK, &iset, &oset);
int count = 0;
sigset_t pending;
while(1)
{
sigemptyset(&pending);
sigpending(&pending);
show_pending(&pending);
sleep(1);
count++;
if(count == 10)
{
//解除对2号信号的屏蔽
sigprocmask(SIG_SETMASK, &oset, NULL);
//2号信号的默认动作是终止进程,所以看不到现象
printf("恢复2号信号,可以被递达了\n");
}
}
return 0;
}
现象我就不演示了,在这里要注意一点,当信号被递达,在pending位图中就会由0置1。
实际上,接收到的信号不一定是马上处理的,有的时候进程正在处理更重要的事,这时信号就会被延时处理。
那信号什么时候被处理(检测,递达(默认、忽略、自定义)?
信号是被保存在进程的PCB中,即pending位图里面。当进程从内核态
返回到用户态
的时候,进行检测和处理工作。
内核态:执行OS的代码和数据时,计算机所处的状态叫做内核态,OS的代码的执行全部都是在内核态。
用户态:用户代码和数据被访问或者执行的时候所处的状态。我们自己写的代码全部都是在用户态执行的!
用户调用系统函数后,除了进入函数,身份也会发生变化,用户身份变成内核身份。
到这可能大家还是很懵,这个用户态和内核态究竟是什么?
前面我们说过每个进程都有它的虚拟地址空间,地址空间有页表可以映射到物理内存上,我们之前所说的页表准确来说是用户级页表,每个进程都有属于自己的用户级页表,而在OS系统中,除了用户级页表还有系统级页表,系统级页表在OS系统中只有一份(即被所有进程共享)。在地址空间的分布中,有1个G的内核空间,这个空间就是通过内核页表映射到物理内存上找到OS的代码和数据。这样设计就能保证既能看到自己写的代码,也能看到OS的代码。那在OS系统中是怎么区分状态的?实际上在OS系统中有一个寄存器CR3
保存着状态,进程具有地址空间是能看到用户和内核的所有内容的,但不一定能访问,能不能访问取决于现在CR3保存的是什么状态。
总结一下就是两点:
1.用户态使用的是用户级页表,只能访问用户数据和代码。
2.内核态使用的是内核级页表,只能访问内核级的数据和代码。
所以现在你就能明白了,所谓的系统调用就是进程的身份转化为内核,然后根据内核页表找到系统函数执行调用。
理解了这些接下来我要放的图你就能更好的理解了。
这就是整个信号处理的过程。
不知道有没有会有疑惑,为什么在进行信号捕捉的时候一定要切回用户态,按理说OS系统拥有的权限更高,完全可以执行用户的代码。其实这是为了安全起见,因为OS系统的身份特殊,一般不会直接去执行用户的代码。
3.4 sigaction
#include <signal.h>
int sigaction(int signo,
const struct sigaction *act,
struct sigaction *oact);
sigaction函数可以读取和修改与指定信号相关联的处理动作。
调用成功则返回0,出错则返回-1。
signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。
若oact指针非空,则通过oact传出该信号原来的处理动作。
act和oact指向sigaction结构体。
将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。 如果
在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask
字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
我们来看看它的使用
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
void handler(int signo)
{
while(1)
{
printf("get a signo: %d\n", signo);
sleep(1);
}
}
int main()
{
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, 3);
//act.sa_handler = SIG_IGN
//act.sa_handler = SIG_DFL;
//本质是修改当前进程的的handler函数指针数组特定内容
sigaction(2, &act, NULL);
while(1)
{
printf("hello world!\n");
sleep(1);
}
return 0;
}
此时会发现2、3号信号都被屏蔽了
4.volatile
我们来看一个场景
#include <stdio.h>
#include <signal.h>
int flag = 0;
void handler(int signo)
{
flag = 1;
printf("change flag 0 to 1\n");
}
int main()
{
signal(2, handler);
while(!flag);
printf("这个进程是正常退出的!\n");
return 0;
}
在编译器有优化的情况下,这段程序运行起来之后,无论你发送几次2号信号,都无法退出,这里是因为做了优化。
我们知道计算是在CPU上进行的,所以flag的值会给CPU去运算,但这段代码编译器检测到flag的值(在主函数里)只是用来检测,并没有修改,干脆省点事,之后直接去访问CPU上存的数据,这就会导致flag的值永远是0,虽然在handler函数里修改了,但只是修改了内存上的flag值。编译器的这种优化显然不是我们想要的,这时就有了volatile
的关键字。
#include <stdio.h>
#include <signal.h>
//此时编译器不会对这个变量做任何优化
volatile int flag = 0;
void handler(int signo)
{
flag = 1;
printf("change flag 0 to 1\n");
}
int main()
{
signal(2, handler);
while(!flag);
printf("这个进程是正常退出的!\n");
return 0;
}
总结一下,volatile的作用是:
让编译器不对此变量做任何优化,读取必须贯穿式的读取内存,不要读取中间缓冲区寄存区中的数据。
5.SIGCHLD信号
我们在学进程时讲过用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻 塞地查询是否有子进程结束等待清理。采用第一种方式,父进程阻塞了就不 能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一下,程序实现复杂。
其实,子进程在终止时会给父进程发SIGCHLD
信号,该信号的默认处理动作是忽略
,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。
事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:
父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN
,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。
系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。
喜欢这篇文章的可以给个一键三连
点赞👍关注💡收藏💖