linux系统编程之基础必备(六):可重入函数、sig
一、 POSIX 中对可重入和线程安全这两个概念的定义:
本文引用地址://m.amcfsurvey.com/article/201610/305552.htmReentrant Function:A function whose effect, when called by two or more threads,is guaranteed to be as if
the threads each executed the function one after another in an undefined order, even if the actual execution is interleaved.
Thread-Safe Function:A function that may be safely invoked concurrently by multiple threads.
Async-Signal-Safe Function: A function that may be invoked, without restriction from signal-catching functions. No function is async-signal -safe unless explicitly described as such.
以上三者的关系为:可重入函数 必然 是 线程安全函数 和 异步信号安全函数; 线程安全函数不一定是可重入函数。
可重入与线程安全的区别体现在能否在signal处理函数中被调用的问题上,可重入函数在signal处理函数中可以被安全调用,因此同时也是Async-Signal-Safe Function;而线程安全函数不保证可以在signal处理函数中被安全调用,如果通过设置信号阻塞集合等方法保证一个非可重入函数不被信号中断,那么它也是Async-Signal-Safe Function。
举个例子,strtok是既不可重入的,也不是线程安全的;加锁的strtok不是可重入的,但线程安全;而strtok_r既是可重入的,也是线程安全的。也就是说函数如果使用静态变量,通过加锁后可以转成线程安全函数,但仍然有可能不是可重入的。我们所熟知的alloc也是线程安全但不是可重入的。
再举个例子,假设函数func()在执行过程中需要访问某个共享资源,因此为了实现线程安全,在使用该资源前加锁,在不需要资源解锁。 假设该函数在某次执行过程中,在已经获得资源锁之后,有异步信号发生,程序的执行流转交给对应的信号处理函数;再假设在该信号处理函数中也需要调用函数 func(),那么func()在这次执行中仍会在访问共享资源前试图获得资源锁,然而我们知道前一个func()实例已然获得该锁,因此信号处理函数阻塞;另一方面,信号处理函数结束前被信号中断的线程是无法恢复执行的,当然也没有释放资源的机会,这样就出现了线程和信号处理函数之间的死锁局面。
因此,func()尽管通过加锁的方式能保证线程安全,但是由于函数体对共享资源的访问,因此是非可重入。
对于这种情况,采用的方法一般是在特定的区域屏蔽一定的信号。
二、可重入函数
我们知道,当捕捉到信号时,不论进程的主控制流程当前执行到哪儿,都会先跳到信号处理函数中执行,从信号处理函数返回后再继续执行主控制流程。信号处理函数是一个单独的控制流程,因为它和主控制流程是异步的,二者不存在调用和被调用的关系,并且使用不同的堆栈空间。引入了信号处理函数使得一个进程具有多个控制流程,如果这些控制流程访问相同的全局资源(全局变量、硬件资源等),就有可能出现冲突,如下面的例子所示。
![](http://m.amcfsurvey.com/editerupload/fetch/20160912/305552_1_0.jpg)
main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant)函数。
不可重入函数的原因在于:
1> 已知它们使用静态数据结构
2> 它们调用malloc和free.
因为malloc通常会为所分配的存储区维护一个链接表,而插入执行信号处理函数的时候,进程可能正在修改此链接表。
3> 它们是标准IO函数.
因为标准IO库的很多实现都使用了全局数据结构
3、sig_atomic_t类型与volatile限定符
在上面的图示例子中,main和sighandler都调用insert函数则有可能出现链表的错乱,其根本原因在于,对全局链表的插入操作要分两步完成,不是一个原子操作,假如这两步操作必定会一起做完,中间不可能被打断,就不会出现错乱了。
关于原子操作最原始的说法是一条汇编指令能够完成(对于多线程程序来说原子操作可以指加锁后的几个步骤集合),即使是一条C语句也不一定是一个原子操作,比如 a = 5; 如果a是32位的int变量,在32位机上赋值是原子操作,在16位机上就不是。如果在程序中需要使用一个变量,要保证对它的读写都是原子操作,应该采用什么类型呢?
为了解决这些平台相关的问题,C标准定义了一个类型sig_atomic_t,在不同平台的C语言库中取不同的类型,例如在32位机上定义sig_atomic_t为int类型。
在使用sig_atomic_t类型的变量时,还需要注意另一个问题。看如下的例子:
#include
sig_atomic_t a=0;
int main(void)
{
/* register a sighandler */
while(!a); /* wait until a changes in sighandler */
/* do something after signal arrives *
/ return 0;
}
为了简洁,这里只写了一个代码框架来说明问题。在main函数中首先要注册某个信号的处理函数sighandler,然后在一个while死循环中等待信号发生,如果有信号递达则执行sighandler,在sighandler中将a改为1,这样再次回到main函数时就可以退出while循环,执行后续处理。如果在编译时加了优化选项,则如果第一次比较a是否为0,如果相等则成了死循环,因为不会再次从内存读取变量a的值。
评论