PHP 死锁问题分析

Posted 21CTO

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了PHP 死锁问题分析相关的知识,希望对你有一定的参考价值。

本文来自云盘服务端团队的技术达人- 徐铁成。一个隐蔽已久的php死锁问题被层层掘出,感谢铁成为我们带来这次畅快的体验,小伙伴们,准备好这次技术之旅了么?

发现问题


近期发现线上很多机器的磁盘空间报警, 且日志文件已经清理,但是磁盘空间没有释放。通过ps aux | grep php-cgi 发现, 很多进程的启动时间在几天到几周甚至几个月之前。我们线上的php-cgi都有最大执行次数的。一般在1天内都会重启一次。初步结论,这些cgi进程有问题。


通过lsof -p [pid] 发现, 启动时间很久的cgi进程中打开了一些日志文件句柄,并且没有关闭。这些日志文件在文件系统中已经删除了。但是句柄没关闭,导致磁盘空间没有释放。到此,磁盘空间异常的问题基本确定。是由于cgi没有关闭文件句柄造成的。


进一步分析进程, strace -p [pid], 发现所有异常的进程都阻塞与 fmutex 状态。换句话所,异常的cgi进程死锁了。进程死锁导致打开的文件句柄没有关闭,所以导致磁盘空间异常。


为什么cgi进程会死锁呢?


什么是死锁


学过操作系统的同学,都了解多线程的概念。在多线程中访问公共资源,需要对资源加锁。访问结束后,释放锁。如果没有释放锁,那么下一个线程来获取资源的时候就会永远都无法获取资源的锁,于是这个线程死锁了。那么CGI是多线程的公共资源访问导致的死锁吗? 答案是NO。

1. CGI 是单线程进程,通过ps 就能看到。(进程状态 Sl的才是多线程进程)。

2. 即使是多线程的,死锁发生在PHP的shutdown过程中调用glibc 中time 函数的位置,不是php模块造成的。而glibc 中的time相关函数是线程安全的,不会产生死锁。


那是什么导致的死锁呢?


通过分析linux中死锁产生的机制,发现除了多线程会产生死锁外,信号处理函数同样会产生死锁。那么cgi是由于信号处理导致的死锁吗?在这之前介绍一个感念。


函数的可重入性与信号安全


函数可重入是指,无论第几次进入该函数,函数都能正常执行并返回结果。那么线程安全函数是可重入的吗?答案是NO。 线程安全函数,在第一次访问公共资源时,会获取全局锁。如果函数没有执行完成,锁还没释放,此时进程被中断。那么在中断处理函数中,再次访问该函数,就会产生死锁。那么什么样的函数才可以在中断处理函数中访问呢? 除了没有使用全局锁的函数,还有一些signal safe的系统调用可以使用。调用任何其他的非signal safe的函数都会产生不可预知的后果(比如 死锁)。 详见 man signal。在分析死锁的原因前,我们先看看cgi执行的流程,分析其中有没有产生死锁的可能。


PHP-CGI的执行流程


Glibc中的时间函数使用到了全局锁,保证函数的线程安全,但没有保证信号安全(signal safe)。经过之前的分析,我们初步怀疑死锁是由于PHP-CGI进程接收到了一个信号,然后在signal handle中执行了非signal safe的函数。主流程在中断前,正在执行glibc中的时间函数。在函数获取的锁没释放前,进入中断流程。而中断过程中又访问了glibc中的时间函数。于是导致了死锁。


PHP-CGI的执行流程,如下图所示:


进一步分析发现,所有死锁的cgi进程的sapi_global中都记录了一个错误信息

“Max execution timeout of 60 seconds exceeded”.

60s 是我们php-cgi中设置执行超时。所以我们确认了,cig在执行过程中的确产生了超时异常,然后由于longjmp进入了shutdown过程。在shutdown过程中访问了glibc中的时间函数。导致了死锁。


void zend_set_timeout(long seconds)

{

TSRMLS_FETCH();


EG(timeout_seconds) = seconds;

if(!seconds) {

return;

}

……


setitimer(ITIMER_PROF, &t_r, NULL);

signal(SIGPROF, zend_timeout); // 此处会调用zend异常处理函数

sigemptyset(&sigset);

sigaddset(&sigset, SIGPROF);


……

}


通过gdb调试发现,所有PHP-CGI都阻塞在zend_request_shutdown中。zend_request_shutdown会调用用户自定义的php脚本中实现的shutdown函数。如果CGI执行超市,那么定时器会产生SIGPROF信号使执行流程中断。如果此时脚本刚好处于调用时间函数的状态,且还没有释放锁资源。然后执行流程进入了 timeout 函数,继续跳转到zend_request_shutdown。此时如果自定义的shutdown函数中访问了时间函数。就会产生死锁。我们从代码中找到了证据:

register_shutdown_function ('SimpleWebSvc:: shutdown’);

我们在php代码中使用qalarm系统,qalarm系统会在cgi执行结束(shutdown)的时候,注入一个钩子函数,来分析cgi执行是否正常,如果不正常,则发送报警信息。而刚好qalarm的报警处理函数中访问了时间函数。于是就有一定的概率产生死锁。


结论


通过上面的分析,我们找到了cgi死锁产生的原因,是应为在signal handler中使用了非signal safe的函数,导致了死锁。


解决办法


去掉或简化qalarm注册到shutdown中的钩子函数。避免不安全的函数调用。


来源:基础架构快报


 关于21CTO平台 

21CTO是中国互联网第一技术学习与服务平台。为CTO、技术总监,技术专家,架构师、技术经理,研发工程师等提供学习成长,工作机会、个人增值等高价值服务。

欢迎您注册为21CTO会员,成为专栏作者,有机会加入专家讲师与技术顾问团队等。一起学习成长,一起用技术改变世界。


以上是关于PHP 死锁问题分析的主要内容,如果未能解决你的问题,请参考以下文章

手把手教你分析Mysql死锁问题

MySQL锁等待与死锁问题分析

MySQL锁等待与死锁问题分析

死锁问题分析,间隙锁

mysql死锁问题分析

MySQL锁等待与死锁问题分析