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

来源:互联网 发布:xmouse软件下载 编辑:程序博客网 时间:2024/05/01 12:33

 

今天遇到了工作以来最深入的问题,就是关于abort函数的多线程安全性问题,应该写一篇文章,纪念一下,希望自己以后能够学到更多关于Linux内核的知识,祝愿自己以后工作越来越顺利。

 

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

 

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

 

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

这里谈论的信号,主要是指异步信号。所谓异步信号,就是随时都有可能出现的信号。从宏观上说,如果一个进程正执行一个函数func(),执行到一半还没有退出的时候,收到一个异步信号,此时,处理器必须转而执行相应的信号处理函数handle(),如果正巧,handle也调用了func(),那就有可能出现问题。例如,func使用了全局变量,如果不加锁,那就和上面提到的多线程安全性问题一样,结果会不一致;如果加锁,试想,在进入handle之前,func已经在该全局变量获得了锁,还没有释放,就进入了handlehandle再次调用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使用其他全局变量或静态变量,所以,abortMT-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时,并没有加锁。因此,严格来讲,abortfclose同时调用,是由潜在危险的。abort应该不是多线程安全的。

 

综上所述,原来对abort的判断是完全错误的,abort不是多线程安全的,但是它是异步信号安全的。

 

疑问:我对递归锁的了解还不是很深入,难道递归锁不会引起单线程内对全局变量或静态变量的访问问题吗?希望有经验的朋友能够帮助我,谢谢。

 

 

 

 

原创粉丝点击