您的位置:首页 > 其它

abort()函数不是多线程安全的,但它是异步信号安全的。

2010-07-26 21:56 218 查看
今天遇到了工作以来最深入的问题,就是关于
abort

函数的多线程安全性问题,应该写一篇文章,纪念一下,希望自己以后能够学到更多关于
Linux

内核的知识,祝愿自己以后工作越来越顺利。

首先,需要说明一下,什么是多线程安全以及异步信号安全。

所谓多线程安全,我们称为
MT-Safe

,就是指同一个函数,同时被多个线程并行调用时,不会出现问题,也就是说,其执行结果就和该函数被串行执行多次的结果一样。打个比方,如果一个函数在不加锁的情况下使用了全局变量或静态变量,那么,当它被多个线程同时调用时,有可能出现结果不一致的情况,这样的函数就不是多线程安全的,我们称为
unsafe



所谓异步信号安全,就是指一个函数可以被一个信号处理函数不加限制地调用,而不会出现问题。这个概念很模糊,一开始我也没有弄懂。要说清楚这个问题,就必须看看什么样的函数不能被信号处理函数安全地调用。

这里谈论的信号,主要是指异步信号。所谓异步信号,就是随时都有可能出现的信号。从宏观上说,如果一个进程正执行一个函数
func()

,执行到一半还没有退出的时候,收到一个异步信号,此时,处理器必须转而执行相应的信号处理函数
handle()

,如果正巧,
handle

也调用了
func()

,那就有可能出现问题。例如,
func

使用了全局变量,如果不加锁,那就和上面提到的多线程安全性问题一样,结果会不一致;如果加锁,试想,在进入
handle

之前,
func

已经在该全局变量获得了锁,还没有释放,就进入了
handle


handle

再次调用
func

,又一次申请同一个锁,这就形成了死锁。所以,这种情况下,无论加不加锁,也就是无论是否多线程安全,这个函数
func

都不是异步信号安全的。

有了上面的铺垫,我们就可以来看一下
abort()

函数的源码了。由于源码太长,我们只列出对本次讨论有帮助的一部分,完整的源码可以在
glibc

源码中
stdlib

文件夹下的
abort.c

文件中找到。

void

abort (void)

{

struct sigaction act;

sigset_t sigs;

/* First acquire the lock.
*/

__libc_lock_lock_recursive (lock);
//

注意,此处是递归锁(
8



......



if (stage == 1)

{

++stage;

fflush (NULL);
//

此处的
fflush

函数引起了
abort

的多线程安全性问题(
13



}

......



}

从代码的第
8

行可以看出,
lock

没有在
abort

中定义,它是一个全局变量,
abort

使用了这个全局变量,但是加锁了,而在余下的代码中,并没有发现
abort

使用其他全局变量或静态变量,所以,
abort


MT-Safe

的。由如同前面所说,由于
abort

加了锁,所以在信号处理函数中调用
abort

,会导致单线程死锁,因此它不是异步信号安全的。

于是,我们得出,
abort

是多线程安全的,但不是异步信号安全的。

可惜,结果正好相反,
abort

不是多线程安全的,而且它是异步信号安全的。我们完全错了。

首先,我们来看一下,为什么
abort

是异步信号安全的。我们认为它不是异步信号安全的,主要是因为第
8

行那个锁,在信号处理函数中调用
abort

会引起死锁。但是,注意到这个锁的名字,
__libc_lock_lock_recursive()

,它是一个递归锁。

所谓递归锁,是
glibc

实现的一种锁机制,这种锁支持函数的递归调用,用来防止单线程死锁。也就是说,使用递归锁,当线程
A

获得了一个锁,线程
B

要想再获得这个锁,是不可能的,但是如果在线程
A

中,又一次申请该锁,那么是可以获得的。递归锁在线程之间的表现,和普通的锁没有区别,但是在同一个线程中,却可以获得多次,当获得了几次就释放了几次以后,该锁才会被线程释放,其他的线程才能够获得该锁。

这样一来,事情就明了了,信号处理函数运行的上下文任然是当前进程,所以当
abort

已经获得了全局变量
lock

上的锁,即使某个信号处理函数再一次调用
abort

申请该锁,它也能得到,而不会引起单线程死锁,因此,
abort

是异步信号安全的。

同时,
abort

又不是多线程安全的,这是为什么呢?难道加了锁,还会引起多线程安全性的问题吗?不,不会,这里的多线程安全性与这个所无关,与死锁也无关,这里的问题是由第
13

行的函数
fflush()

引起的。

参看源码可知,此处的
fflush

并不是
glibc

中实现的
fflush

函数,而是一个宏,

# define fflush(s) _IO_flush_all_lockp (0)

这个宏调用了
_IO_flush_all_lockp()

函数,问题就处在
_IO_flush_all_lockp

里面。

int

_IO_flush_all_lockp (int do_lock)

{

......



while (fp != NULL)

{

run_fp = fp;
//

此处
run_fp

是一个全局变量,
fp

是局部变量

if (do_lock)

_IO_flockfile (fp);

......



if (do_lock)

_IO_funlockfile (fp);

run_fp = NULL;

......



}

请先仔细看一下上面的代码逻辑,
do_lock


_IO_flush_all_lockp

函数的参数,
fp

是一个局部指针,只想一个文件描述符,只有在
do_lock

不为空时,
_IO_flush_all_lockp

函数才对
fp

加锁。

我们知道,虽然
fp

是一个局部指针,但是它所指向的文件描述符可能被多个线程使用,当
_IO_flush_all_lockp

函数判断了
fp != NULL

以后,进入循环,这是,如果没有对
fp

加锁,很有可能会有另一个线程调用了
fclose()

函数将
fp

指向的这个文件描述符关闭,如此一来,当再一次回到
_IO_flush_all_lockp

的逻辑时,
fp

实际上已经为空了,再对它进行操作,是很危险的。

我们再看一下
abort

的第
13

行代码,发现
abort

调用
fflush(s)

宏,它传给
_IO_flush_all_lockp

的参数
do_lock

就是
NULL

,因此,实际上,
_IO_flush_all_lockp

在使用
fp

时,并没有加锁。因此,严格来讲,
abort


fclose

同时调用,是由潜在危险的。
abort

应该不是多线程安全的。

综上所述,原来对
abort

的判断是完全错误的,
abort

不是多线程安全的,但是它是异步信号安全的。

疑问:我对递归锁的了解还不是很深入,难道递归锁不会引起单线程内对全局变量或静态变量的访问问题吗?希望有经验的朋友能够帮助我,谢谢。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: