信号量内部实现原理:
举个例子,前台运行某个程序的时候,我们在键盘输入 Ctrl+C,就会终止这个进程,这是因为前台进程捕捉了这个有键盘传送过来的信号,并执行了相应的处理程序,那么什么又是信号捕捉呢?
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:
1. 用户程序注册了SIGQUIT信号的处理函数sighandler。
2. 当前正在执行main函数,这时发生中断或异常切换到内核态。
3. 在中断处理完毕后要返回用户态的main函数之前检查到有信SIGQUIT递达。
4. 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,
是 两个独立的控制流程。
5. sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。
6. 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
所以我们可以看到一次信号捕捉有四次状态切换:用户->内核->用户->内核->用户,这里我们需要注意的点是:上述的第四点,在此强点一下,执行处理信号的函数的时候,并不是回到main函数的堆栈中执行,而是是直接到这个程序的代码处执行,并且是以用户态的权限。
所以相当于在信号发生时,该进程的控制权被转交给信号处理函数,只有等到信号处理函数完成后,才会将控制权再交还给main函数。
可重入函数
- 单进程多次执行,即信号那种方式,加入有如下代码:
void sum(int a,int b){
return a+b;
}
void catch_signal(int sign)
{
sum(1,2); //1
}
int main(int arg, char *args[])
{
//注册终端中断信号
signal(SIGINT, catch_signal);
while(1){
sum(3,4); //2
}
return 0;
}
当main执行到2时,按下ctrl+c,信号发生,进入catch_signal函数,而catch_signal接下来执行到1,又进入sum函数内。这种就叫单进程重复执行。如果sum为可重入函数,这种情况下要保证结果不受上次影响。
2. 多线程或者多进程重复执行,不受上次影响。
线程安全
线程安全只要满足上面可重入的第2个要求即可。
可重入与线程安全的关系
所以说可重入的一定是线程安全,但线程安全不一定是可重入,它可以通过加锁实现的,而这种加锁实现的线程安全不是可重入的。
例如malloc,printf等等函数就是这种通过加锁实现的线程安全函数,但却不是可重入的。
例如malloc在执行时,会访问一个全局状态,而为了能够实现线程安全,所以malloc在访问之前都会加锁。
printf也是类似的。
将上面的代码的1,2处改为malloc,大家就知道为什么malloc是线程安全但却不是可重入的,因为它不满足可重入的第一个要求,单进程多次执行会出错。
例如执行到2处malloc时,按下ctrl+c,进程调到catch_signal函数中执行,执行到1处,又是malloc函数,但是由于2处执行malloc时,已经对全局状态加锁了,所以在1处时,就会发生死锁。