Linux:理解阻塞信号与函数重入


信号的阻塞

指一个信号的递达,信号依然可以注册,只是暂时不处理(未决状态)直到进程解除对此信号的阻塞,才执行递达的动作.

在内核中的表示

实际执行信号的处理动作称为信号递达(Delivery ),信号从产生到递达之间的状态,称为信号未决(Pending)。进程可以选择阻塞(Block)某个信号,被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。

注意:

阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。信号在内核中的表示可以看作是这样的:

在这里插入图片描述

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

  1. SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。

  2. SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。

  3. SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。

未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

信号集处理函数

sigset_t类型(64bit)对于每种信号用一个bit表示“有效”或“无效”状态。

#include <signal.h>

int sigemptyset(sigset_t *set) // 清空set信号集合 - 使用一个变量的时候的初始化过程
int sigfillset(sigset_t *set) //将所有信号添加到set集合中
int sigaddset(sigset_t *set, int signum) // 向set集合中添加指定的信号
int sigdelset(siget_t *set, int signum) // 从set集合中移除指定的信号
int sigismember(const sigset_t set, int signum) // 判断制定信号是否在set集合中

注意:

  • 在使用sigset_t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。
  • 上面四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。

sigprocmask函数

int sigprocmask(int how, sigset_t *set, sigset_t *old);

参数:

如果old是非空指针,则读取进程的当前信号屏蔽字通过old参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果old和set都是非空指针,则先将原来的信号屏蔽字备份到old里,然后根据set和how参数更改信号屏蔽字。

  • how:当前要对block集合进行的操作
    • SIG_BLOCK 将set集合中的信号添加到block进程阻塞信号集合
      block = block | set
      表示阻塞 set集合中的信号以及原有的阻塞信号,并且将原有的阻塞信号返回到old集合中(便于还原)
    • SIG_UNBLOCK 将set集合中的信号从block集合中移除,将set集合中的信号解除阻塞
      block = block & (~set)
    • SIG_SETMASK 将内核中的block集合中的信号设置为set集合中的信号
      block = set

返回值:若成功则为0,若出错则为-1

sigpending函数

#include <signal.h>

int sigpending(sigset_t *set);

sigpending读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。

在所有信号中,有两个信号不可被阻塞,不可被自定义修改处理方式,也不可被忽略
这两个信号:SIGKILL-9 / SIGSTOP-19

简介信号 SIGPIPE、SIGCHLD

  1. SIGPIPE信号

管道的博文中,介绍到了:

所有管道读端被关闭,则继续写入会触发异常
此时的异常调用的就是SIGPIPE(Mac终端下是PIPE)

  1. SIGCHLD信号:

信号产生:

僵尸进程:子进程退出后,操作系统发送SIGCHLD信号给父进程,但是因为SIGCHLD信号的默认处理方式就是忽略,因此在之前的程序中并没有感受到操作系统的通知
因此只能固定的使用进程等待来避免产生僵尸进程,但是在这个过程中父进程是一直阻塞的,只能一直等待子进程退出。

如何让程序感知到操作系统的通知??

就在程序初始化阶段,将SIGCHLD信号的处理方式自定义,并且在自定义函数重调用waitpid,这样的话就当子进程退出的时候,则自动回调处理了,父进程就不需要一直等待了。

多个子进程同时退出,都会向父进程发送SIGCHLD信号,但是SIGCHLD信号是非可靠信号,有可能会丢失事件,

例如:

三(多)个进程同时退出,但是信号只注册了一次
意味着只会执行一次回调函数,调用一次waitpid,只能处理一个僵尸进程
非可靠信号的丢失是无法避免的,因此只能在一次信号回调中处理完所有的僵尸进程

解决方法:

while(waitpid(-1, NULL, WNOHANG)>0);  非阻塞循环在一个回调中将所有的僵尸进程全部处理
  • waitpid返回值: >0 -退出子进程的pid / ==0 -没有子进程退出 / <0 -出错

  • 循环是为了若有子进程退出则一直处理,直到没有子进程退出,则退出循环信号回调完毕

  • WNOHANG-将waitpid设置为非阻塞,没有子进程退出的时候返回0,退出循环,不要导致程序流程卡在信号回调函数中

volatile 关键字

修饰一个变量,使这个变量保持内存可见性—每次对变量访问(cpu处理数据)都需要重新从内存加载变量的数据,防止编译器过度优化(gcc -O0/1/2/3)

  • cpu处理一个数据的过程时从内存中将数据加载到寄存器上进行处理
  • gcc编译器,在编译程序的时候,如果使用了代码优化 -Olevel 选项,发现某个变量使用频率非常高,为了提高效率,则直接将变量的值设置为某个寄存器的值,以后访问的时候直接从寄存器访问,则减少了内存访问的过程提高了效率。
  • 但是这种优化有时候会造成代码的逻辑混乱

因此使用volatile关键字修饰变量,让cpu无论如何每次都重新倒内存中获取数据

可重入函数

竞态条件: 在多个执行流中,程序的竞争执行

函数的重入

同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。

函数的可重入与不可重入区别:

  • 函数的可重入:在函数重入之后,不会造成数据二义或者引起程序逻辑混乱,则这个函数是一个可重入函数
  • 函数的不可重入:在函数重入之后,有可能会造成数据二义或者额引起程序逻辑混乱,这个函数是一个不可重入函数

函数是否可重入的判定基准:

  • 一个函数中是否对全局数据进行了非原子性的操作(若有则不可重入)
  • 原子性:操作要不然一次性完成,要不然就不做(操作不可被打断)

可重入函数:

  • 一个函数若根本就没有操作全局数据(或者静态数据),则肯定是可重入的,因为每个函数调用的时候都有独立的函数栈
  • 一个函数若对全局数据进行了操作,但是操作时原子性的,则也是可重入的

当我们以后在执行多个执行流中使用别人的函数的时候(包括自己封装实现函数的时候),就需要考虑这个函数是否重入,所有可能给程序带来的不确定性。

不可重入函数:

  • malloc/free 都是不可重入函数,因为malloc也是用全局链表来管理堆的。因此在多个执行流中进行操作应该小心
  • 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
  • 可重入函数体内使用了静态的数据结构

可重入与线程安全联系

  • 函数是可重入的,那就是线程安全的
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

可重入与线程安全区别

  • 可重入函数是线程安全函数的一种
  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

如果本篇博文有帮助到您,请留个赞激励博主呐~~

猜你喜欢

转载自blog.csdn.net/AngelDg/article/details/106822083