一、 POSIX 中对可重入和线程安全这两个概念的界说:
Reentrant 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()虽然经过加锁的办法能确保线程安全,可是由于函数体对共享资源的拜访,因而对错可重入。
关于这种状况,选用的办法一般是在特定的区域屏蔽必定的信号。
二、可重入函数
咱们知道,当捕捉到信号时,不管进程的主操控流程当时履行到哪儿,都会先跳到信号处理函数中履行,从信号处理函数回来后再持续履行主操控流程。信号处理函数是一个独自的操控流程,由于它和主操控流程是异步的,二者不存在调用和被调用的联系,而且运用不同的仓库空间。引入了信号处理函数使得一个进程具有多个操控流程,假设这些操控流程拜访相同的大局资源(大局变量、硬件资源等),就有或许呈现抵触,如下面的比方所示。
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的值。