Linux的进程信号(下)

Linux的进程信号(下)_第1张图片

文章目录

  • 1. 阻塞信号
    • 1.1 信号其他相关常见概念
    • 1.2 在内核中的表示
  • 2. sigset_t
  • 3. 信号集操作函数
    • 3.1 sigprocmask
    • 3.2 sigpending
    • 3.3. 实例演示
  • 4. 信号的处理
    • 4.1. sigaction
    • 4.2 多个信号的处理
  • 5. 可重入函数
  • 6. volatile
  • 7. SIGCHLD信号

1. 阻塞信号

1.1 信号其他相关常见概念

1.实际执行信号的处理动作称为信号递达(默认,忽略,自定义捕捉)。
2.信号从产生到递达之间的状态,称为信号未决。
3.进程可以选择阻塞 (Block)某个信号。
4.被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞才执行递达的动作。
5.阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

1.2 在内核中的表示

信号在内核中的示意图:
Linux的进程信号(下)_第2张图片
这里的pending就是前面说的位图,用来识别信号。handler就是函数指针数组,用来处理信号的。block(阻塞方法集)也是位图,它和pending的结构是一样的,几号bit位代表几号信号,表示该信号是否阻塞

每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。

在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号。因为进程仍有机会改变处理动作之后再解除阻塞。
SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。

如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理
Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。

2. sigset_t

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储。sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

3. 信号集操作函数

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现。从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做
任何解释,比如用printf直接打印sigset_t变量是没有意义的。
Linux的进程信号(下)_第3张图片
1.函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
2.函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位设为1,表示该信号集的有效信号包括系统支持的所有信号。
3.函数sigaddset是添加某个信号到这个信号集中。
4.函数sigdelset是删除某个信号。
5.函数sigismember是判断某个信号是否在信号集中

注意:在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号

3.1 sigprocmask

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集,也就是block表)
在这里插入图片描述
假设当前的信号屏蔽字为mask,下表说明了how参数的可选值:
Linux的进程信号(下)_第4张图片
这里第二个参数set就是我们用户需要输入的,第三个参数是OS返回给用户的。如果oset是非空指针,则读取进程的当前信号屏蔽字(原来的)通过oset参数传出。

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达

3.2 sigpending

Linux的进程信号(下)_第5张图片
这是一个输出型参数,读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。

3.3. 实例演示

Linux的进程信号(下)_第6张图片
当我们获取当前信号集的时候,我们把它打印出来。
Linux的进程信号(下)_第7张图片
打印函数我们要自己写一下:
Linux的进程信号(下)_第8张图片
我们先看一下运行结果:
Linux的进程信号(下)_第9张图片
我们可以看到一开始都是0,现在我们给这个进程发2号信号:
在这里插入图片描述
在这里插入图片描述
可以看到它并没有把对应的位置打印出来,直接结束进程了。那么我们就需要将2号信号给捕捉一下:
Linux的进程信号(下)_第10张图片
Linux的进程信号(下)_第11张图片
现在我们再看一下运行情况:
Linux的进程信号(下)_第12张图片
这里收到了信号,但是对应的pending信号集里还是没有,因为CPU很快就处理了这个信号,我们看不到。
所以,当我们发送2号信号时,让2号信号block:
Linux的进程信号(下)_第13张图片
这里可以看到,2号信号被阻塞了,但发送了2号信号,对应的pending信号集已经变成1了。

既然我们给它阻塞了,那么我们就需要恢复它。
Linux的进程信号(下)_第14张图片
运行结果如下:
Linux的进程信号(下)_第15张图片

4. 信号的处理

之前说过,进程处理信号,不是立即处理,是在合适的时候,那么到底是什么时候呢
当当前进程从内核态,切换回用户态的时候,进行信号的检测与处理

那么什么是内核态和用户态呢
Linux的进程信号(下)_第16张图片
前面我们说过,进程的会通过虚拟地址空间和页表把用户空间(3G)映射到物理内存。那么这里的页表叫做用户级页表,每一个进程,都有一份,但是大家的用户级页表都是不一样的

其实在OS还有一份页表叫做内核级页表,它映射的是内核空间(1G),这个页表所有进程共享一份

我们知道无论进程怎么切换,我们都可以找到内核的代码和数据,前提是你要有权利访问。

那么我们怎么知道当前进程如何具备权利,访问这个内核页表,乃至访问内核数据呢
要进行身份切换,进程如果是用户态,那么只能访问用户级页表,进程如果是内核态,可以访问内核级和用户级页表

那么怎么知道当前进程是用户态还是内核态呢
CPU内部有对应的状态寄存器,可以用bit位标识当前进程的状态0代表内核态,3代表用户态

什么时候进入到内核态呢
系统调用的时候或者时间片到了,进程间切换

我们的程序会无数次直接或者间接的访问系统软硬件资源(管理者是OS),本质上,你并没有自己去操作这些软硬件资源,但是通过了OS,从而无数次的陷入内核(1.切换身份,2.切换页表),然后调用内核的代码,从而完成访问动作,把结果返回给用户(1.切换身份,2.切换页表)

举个例子:
Linux的进程信号(下)_第17张图片
在我们的代码中写了一个系统调用open,当执行到open时,就会进入内核态,当执行完open函数时,OS会检测进程的PCB,检测PCB里面的信号(block&&pending),假设block为0,pending为1,那么就会去执行对应的handler,假设handler是我们自己定义的方法:
Linux的进程信号(下)_第18张图片
但是当我们执行完自己的handler方法时,它会直接去执行open后面的代码吗
答案是:不会。原因是:我们是执行open函数时遇到信号了,当处理完信号后,open并没有返回值,所以我们不能直接执行open后面的代码。而是先回到内核态通过处理再回到用户态执行open后面的代码。

那么这里是以谁的身份去执行这个handler呢
可能大家会认为是内核身份,虽然内核态可以去执行,但是它不愿意。这里只能用户级身份来执行。原因是:这个handler是我们自己写的,如果在里面我们写了一段恶意代码,让内核去执行,那么就可能造成问题。

完整过程
Linux的进程信号(下)_第19张图片

4.1. sigaction

在这里插入图片描述
sigaction函数可以读取和修改与指定信号相关联的处理动作,也就是获取和修改handler表。调用成功则返回0,出错则返回- 1。signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体

这个函数和signal功能是差不多的,但是signal比较简单。

那么我们先看一下sigaction的结构体
Linux的进程信号(下)_第20张图片
我们暂时先考虑这两个参数,其它的暂时不考虑。

代码演示:
Linux的进程信号(下)_第21张图片
这里我们设置了一个自定义捕捉动作。
Linux的进程信号(下)_第22张图片
当收到信号时,完成了自定义捕捉动作。
将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字。这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止

举个例子:
Linux的进程信号(下)_第23张图片
运行结果:
在这里插入图片描述
首先执行的是主函数里面的sleep。
Linux的进程信号(下)_第24张图片
当我们发送2号信号时,它就会一直处理。
Linux的进程信号(下)_第25张图片
当我们再次发送2号信号,它就会屏蔽了。

那么这个sigaction里面的maks到底是什么呢
如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字
Linux的进程信号(下)_第26张图片
运行结果:
Linux的进程信号(下)_第27张图片
现在我们想要结束进程,我们可以使用kill -9,也可以使用killall 进程名
在这里插入图片描述
Linux的进程信号(下)_第28张图片

4.2 多个信号的处理

方法一:
Linux的进程信号(下)_第29张图片
这样就可以调用同一个函数来执行不同的方法。然后我们把捕捉方法写好就行:
Linux的进程信号(下)_第30张图片
测试结果:
Linux的进程信号(下)_第31张图片
方法二:
在这里插入图片描述
这里可以使用一个vector来把所有方法push进去,但是我们需要自己把所有函数捕捉方法写好。所以我们更推荐 unordered_map。
在这里插入图片描述

5. 可重入函数

Linux的进程信号(下)_第32张图片
这里是一个单链表,在main函数里面调用插入函数,并且在信号捕捉里也调用了这个插入函数。
Linux的进程信号(下)_第33张图片
我们执行main函数里面的插入时,假设时间片到了,只完成了第一步,没有完成head=p,那么此时用户就会从用户态切换成内核态。那么在内核态就会检测信号,然后就会执行我们的handler方法。
Linux的进程信号(下)_第34张图片
那么handler方法没人打扰,就会执行完。
Linux的进程信号(下)_第35张图片
执行完后,再去我们自己的代码执行head=p。
Linux的进程信号(下)_第36张图片
insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入

insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数。反之,如果一个函数只访问自己的局部变量或参数,则称为可重入函数

如果一个函数符合以下条件之一则是不可重入的:
1.调用了malloc或free,因为malloc也是用全局链表来管理堆的。
2.调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构

6. volatile

该关键字在C++当中我们已经有所涉猎,今天我们站在信号的角度重新理解一下:
Linux的进程信号(下)_第37张图片
在没有收到2号信号的时候一直在循环里判断,当收到2号信号的时候,就退出循环。
Linux的进程信号(下)_第38张图片
这是正常的理想状态下。
Linux的进程信号(下)_第39张图片
这里O2是告诉编译器进行优化。
Linux的进程信号(下)_第40张图片
还是同样的代码,但是现在循环并没有退出。

这里的优化是把flags的值放到了CPU的寄存器中,在逻辑计算的时候,就不会从内存中取数据,而是直接从CPU寄存器里取数据。那么为了防止这种情况,我们可以加个volatile。
Linux的进程信号(下)_第41张图片
Linux的进程信号(下)_第42张图片
又可以正常退出了。

volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作

7. SIGCHLD信号

在这里插入图片描述
在子进程退出的时候,自动给父进程发送SIGCHLD信号

证明如下
Linux的进程信号(下)_第43张图片
我们对子进程暂停或者退出,看父进程能否捕捉信号。
Linux的进程信号(下)_第44张图片
现在我们暂停子进程:
Linux的进程信号(下)_第45张图片
我们再恢复子进程:
Linux的进程信号(下)_第46张图片
我们再删除子进程:
在这里插入图片描述
我们可以看到以上三种情况,父进程都可以收到SIGCHLD信号。

那么子进程给父进程发送信号有什么作用呢
Linux的进程信号(下)_第47张图片
这是我们之前写的一个父进程等待子进程,但是都是父进程要自己主动等待。
Linux的进程信号(下)_第48张图片
现在我们可以让父进程去做自己的事情,不主动等待:
Linux的进程信号(下)_第49张图片
我们让父进程做自己的事情,并且捕捉信号。
Linux的进程信号(下)_第50张图片
我们可以看到:父进程和子进程一开始都在运行,然后子进程退出的时候,父进程一直做自己的事情,并且自动等待成功。

但是这个代码有一些bug
Linux的进程信号(下)_第51张图片
如果我们一次性创建多个进程,当进程退出时给父进程发送信号,就可能造成多个进程同时退出。而Linux处理一次信号时,其它信号可能就被阻塞了,那么其它进程就不会被等待回收,一直就是僵尸进程。
Linux的进程信号(下)_第52张图片
我们循环不断waitpid去等待子进程,当所有子进程都没了,就退出循环。

但是这里还存在一些问题:
Linux的进程信号(下)_第53张图片
这里的意思是:前8个运行5秒,后2个子进程运行20秒。这种情况会造成后两个子进程在运行时,父进程会在waitpid被阻塞等待。
Linux的进程信号(下)_第54张图片
那么父进程就不能执行自己的代码了。我们需要改成WNOHANG
Linux的进程信号(下)_第55张图片
当参数为WNOHANG时,id等于0的时候,说明子进程还有没退出完的。

事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用sigaction或者signal将SIGCHLD的处理动作置为SIG_IGN。这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用
Linux的进程信号(下)_第56张图片
设置过后,父进程就不需要任何处理了,OS就会把所有僵尸状态的子进程都给忽略掉。

子进程退出的时候,默认的信号处理就是忽略,那么调用signal/sigaction设置为SIG_IGN,,意义在哪里呢

SIG_IGN手动设置,让子进程退出,不要给父进程发送信号了,并且自动释放

你可能感兴趣的:(Linux,linux,进程的信号)