声明:本文为个人理解,不能保证一定是正确的!
这三个概念一直纠缠着我,我也时不时的会拿出来辨析下,直到昨天才发现自己可以把它们理顺了。所以学习就是这样一个反复的过程,最终达到顿悟的效果。本文主要参考APUE第三版英文版第10.6和12.5节,以及WIKI百科,还有CSDN和*中对这些概念的讨论,然后给出一份自己认为比较合理的理解。
中断,信号,线程切换
==========================
-
这三个概念都牵涉到异步通信,因为运行中的代码不可预测什么时候会发生中断,什么时候会收到信号,什么时候会发生线程切换:
- 中断,一般指的硬件中断,是硬件对cpu的中断
- 信号,则是对中断的模拟,可以看作是os对进程的中断
- 线程切换,cpu的时分复用手段
代码执行流:
#-表示代码在cpu上运行,.表示等待中
##1. 中断和信号
<f1>---------...................-----------
<f2>.........-------------------...........
###f1在执行的过程中被f2打断,当且仅当f2完整的执行完后返回f1
##2. 线程切换
<f1>----....----....----....----....----
<f2>....----....----....----....----....
###f1和f2互相打断彼此,交错地运行
可重入,异步信息安全,线程安全
====================================
可重入
可重入的意思就是一个函数没有执行完,又在另一个地方被调用一次,两次调用都能得到正确的结果。可重入概念在多任务操作系统之前就已经存在了,It is a concept from the time when no multitasking operating systems existed。-
异步信息安全,线程安全
可重入中提到的”另一个地方”可以是:- 在中断或信号中,在此情况下如果函数是可重入的,那么就称这个函数是异步信号安全的,这两种情况可以看成是一种递归调用。
[APUE10.6节就是专门说明异步信号安全的] - 在另一个线程中,在此情况下如果函数是可重入的,那么就称这个函数是线程安全的。
[APUE12.5节就是专门说明线程安全的]
If a function is reentrant with respect to multiple threads, we say that it is thread-safe. This doesn’t tell us, however, whether the function is reentrant with respect to signal handlers. We say that a function that is safe to be reentered from an asynchronous signal handler is async-signal safe.
- 在中断或信号中,在此情况下如果函数是可重入的,那么就称这个函数是异步信号安全的,这两种情况可以看成是一种递归调用。
3. 三者的关系
红色表示不可重入函数,A+B+C表示可重入函数,其中A表示递归调用情况下可重入,C表示多线程的情况下可重入,B表示种情况下都可重入。
所以单独说某函数是重入,而不限定使用场景是没有意义的;如果仅仅说是非可重入则又是有意义的…
4. 异步信号安全函数列表
5. 可重入函数和异步信号安全函数等同吗?
根据上面的关系图可以得出两者是不等同的,可重入函数在不同的情景下可以分为异步信号安全和线程安全。异步信号安全可以看作是递归调用情况下的可重入。举例如下:
//非可重入版本
int t;
void swap(int *x, int *y)
{
t = *x;
*x = *y;
// hardware interrupt might invoke isr() here!!
*y = t;
}
/////////////////////////////////////////////
//异步信号安全版本,非线程安全
int t;
void swap(int *x, int *y)
{
int s;
s = t; // save global variable
t = *x;
*x = *y;
// hardware interrupt might invoke isr() here!
*y = t;
t = s; // restore global variable
}
//在信号处理函数中调用时,每次使用t之前都会进行备份,返回之前还原t的值;所以在信号处理函数中调用是可重入的;
//如果用在线程中,则有可能线程切换之前未来得及还原t的值,导致结果出错,所以在线程切换中是不可重入的;
//由此可以看出,异步信号安全函数不一定是线程安全函数!
//线程安全的函数由于可能使用互斥锁,在信号处理函数中递归调用会出现死锁,所以线程安全的函数也不一定是异步信号安全的。
其它
=====
-
信号就像硬件中断一样,会打断正在执行的指令序列。信号处理函数无法判断捕获到信号的时候,进程在何处运行。如果信号处理函数中的操作与打断的函数的操作相同,而且这个操作中有静态数据结构等,当信号处理函数返回的时候(当然这里讨论的是信号处理函数可以返回),恢复原先的执行序列,可能会导致信号处理函数中的操作覆盖了之前正常操作中的数据。
所以通常函数不可重入的原因在于:- 函数使用静态数据结构;
- 函数调用malloc和free.因为malloc通常会为所分配的存储区维护一个链接表,而插入执行信号处理函数的时候,进程可能正在修改此链接表;
- 函数是标准IO函数,因为标准IO库的很多实现都使用了全局数据结构;
- 函数会修改自身代码,导致多次调用不同代码;
- 等等。
即使对于可重入函数,在信号处理函数中使用也需要注意一个问题就是errno。一个线程中只有一个errno变量,信号处理函数中使用的可重入函数也有可能会修改errno。例如,read函数是可重入的,但是它也有可能会修改errno。因此,正确的做法是在信号处理函数开始,先保存errno;在信号处理函数退出的时候,再恢复errno。
例如,程序正在调用printf输出,但是在调用printf时,出现了信号,对应的信号处理函数也有printf语句,就会导致两个printf的输出混杂在一起。
如果是给printf加锁的话,同样是上面的情况就会导致死锁。对于这种情况,采用的方法一般是在特定的区域屏蔽一定的信号。
屏蔽信号的方法:
1> signal(SIGPIPE, SIG_IGN); //忽略一些信号
2> sigprocmask()
sigprocmask只为单线程定义的
3> pthread_sigmask()
pthread_sigmasks可以在多线程中使用很多函数并不是线程安全的,因为他们返回的数据是存放在静态的内存缓冲区中的。通过修改接口,由调用者自行提供缓冲区就可以使这些函数变为线程安全的。操作系统实现支持线程安全函数的时候,会对POSIX.1中的一些非线程安全的函数提供一些可替换的线程安全版本。
例如,gethostbyname()是线程不安全的,在Linux中提供了gethostbyname_r()的线程安全实现。
函数名字后面加上”_r”,以表明这个版本是可重入的(对于线程可重入,也就是说是线程安全的,但并不是说对于信号处理函数也是可重入的,或者是异步信号安全的)。-
多线程程序中常见的疏忽性问题
- 将指针作为新线程的参数传递给调用方栈,我就犯过这样的错…
- 在没有同步机制保护的情况下访问全局内存的共享可更改状态。
- 两个线程尝试轮流获取对同一对全局资源的权限时导致死锁。其中一个线程控制第一种资源,另一个线程控制第二种资源。其中一个线程放弃之前,任何一个线程都无法继续操作。
- 尝试重新获取已持有的锁(递归死锁)。
- 在同步保护中创建隐藏的间隔。如果受保护的代码段包含的函数释放了同步机制,而又在返回调用方之前重新获取了该同步机制,则将在保护中出现此间隔。结果具有误导性。对于调用方,表面上看全局数据已受到保护,而实际上未受到保护。
- 将UNIX 信号与线程混合时,使用sigwait(2) 模型来处理异步信号。
- 调用setjmp(3C) 和longjmp(3C),然后长时间跳跃,而不释放互斥锁。
- 从对*_cond_wait() 或*_cond_timedwait() 的调用中返回后无法重新评估条件。
如果一个函数对多个线程来说是可重入的,则说这个函数是线程安全的,但这并不能说明对信号处理程序来说该函数也是可重入的。
如果函数对异步信号处理程序的重入是安全的,那么就可以说函数是”异步-信号安全”的。
参考文档
==============
1. APUE第三版原版10.6&12.5,去阅读原版而不是中文版(有道在手,天下我有:P)
2. https://en.wikipedia.org/wiki/Reentrancy_(computing)
3. http://bbs.csdn.net/topics/310140183
4. http://*.com/questions/9837343/difference-between-thread-safe-and-async-signal-safe
5. http://*.com/questions/18198487/are-r-unix-calls-reentrant-async-signal-safe-thread-safe-or-both