死锁的发生
自己所在的团队在开发新版本过程中,一次测试环境发生了server死锁,整个server的任务线程都被hang住。而死锁的代码就在我负责的程序日志部分中localtime_r函数调用处。
程序日记需要记录打印日志的时间,而localtime_r函数就是用于将系统时间转换为本地时间。同样功能的函数还有localtime。两个函数的区别是:localtime_r是thread-safe,其返回的结果存在由用户提供的buffer中;而localtime返回的结果是指向static变量,多线程环境可被其他线程修改。localtime_r实现中有一把锁,负责lock tzfile中的状态变量,而server就在这里发生死锁。
经过分析死锁是由于发kill信号,信号处理函数引起的。原线程打印程序日志获得localtime_r中需要的锁后,kill信号触发中断处理,正好分配给该线程处理中断。信号处理函数中再次打印日志,调用localtime_r的锁时发生死锁。
之前的信号处理方式为异步方式,同时信号处理函数中做了很多事情。之前大家一直关注线程安全,却从来没有注意过异步信号处理函数的安全性。所在项目之前的信号处理函数实现一直是这个方案,但这次最新版本由于还在开发中,大家调用了大量日志打印,增加了死锁的概率才将这个问题暴露出来。这也暴露了部分代码场景思考不充分,测试不足。
Signal Handling and Nonreentrant Functions
信号处理函数不推荐做太多工作,如果调用函数需要是reentrant。reentrant可重新进入的,可以理解为一次调用发生后,不会对该函数的再次调用发生任何影响。即reentrant函数中不可以有static或global变量,不可以分配释放内存,通常不可以使用修改用户提供的对象,修改errno等等。
具体可以看
http://www.gnu.org/software/libc/manual/html_node/Nonreentrancy.html#Nonreentrancy
解决信号处理带来的死锁 异步变同步
自己的第一直觉是既然信号处理函数不可以做太多工作,需要调用non-reentrant函数,那就把日志打印全部去掉好了。但发现,所在项目的信号处理函数中会做大量工作,许多调试方法和调试信息通过kill信号获得,而且这些调用基本都是non-reentrant。所以只能修改信号处理的方案。
信号处理的方式除了异步使用方式还有同步使用方式。同步信号处理方式即指定线程以同步的方式对从信号队列中获取信号进行处理。主要调用函数为sigwait,流程:
1、主线程设置信号掩码,设置希望同步处理的信号;主线程的信号掩码会被创建的线程继承;
2、创建信号处理线程,信号处理线程循环调用sigwait(sigtimedwait)等待希望同步处理的信号并做信号处理;
3、创建其他线程。
可参考http://www.ibm.com/developerworks/cn/linux/l-cn-signalsec/
异步信号处理死锁复现测试
#include <gtest gtest.h="">
#include <signal.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>
//signal function should not call I/O function and nother non-reentrant functions
//this test would have a dead lock. Because localtime_r have a lock. When signal interrupt
//occurs, if the main function get the lock and had not released it, the dead lock produced.
void handler(int signum)
{
char result[100];
time_t now;
struct tm time1;
now = time(NULL);
localtime_r(&now, &time1);
strftime(result, 100, "%T", &time1);
printf("At %s, user pressed Ctrl-C\n", result);
}
int main (void)
{
time_t now;
struct tm ltime;
if (signal(41, handler) == SIG_IGN)
signal(41, SIG_IGN);
now = time(NULL);
while(1) {
localtime_r(&now, <ime);
}
return 0;
}
同步信号处理方式
程序中tbsys使用见http://blog.csdn.net/michaelyang_yz/article/details/49056213
#include <gtest gtest.h="">
#include <signal.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>
#include "tbsys.h"
namespace test {
bool g_run_test = false;
void signal_handler(int signum)
{
printf("singal num: %d", signum);
char result[100];
time_t now;
struct tm time1;
now = time(NULL);
localtime_r(&now, &time1);
strftime(result, 100, "%T", &time1);
printf("At %s, user pressed Ctrl-C\n", result);
}
class ObSignalDealThread: public tbsys::CDefaultRunnable
{
public:
virtual void run(tbsys::CThread *thread, void *arg);
};
class ObSignalTest: public ::testing::Test
{
public:
void run_test();
protected:
ObSignalDealThread signal_deal_thread_;
};
void ObSignalDealThread::run(tbsys::CThread *thread, void *arg)
{
UNUSED(thread);
UNUSED(arg);
sigset_t waitset;
intsignum;
sigemptyset(&waitset);
sigaddset(&waitset, SIGINT);
struct timespec timeout = {1, 0};
while (!_stop) {
if ( -1 == (signum = sigtimedwait(&waitset, NULL, &timeout))) {
//do not log error, because timeout will also return -1.
printf("time out or error, errno=%d, errmsg=%s\n", errno, strerror(errno));
} else {
printf("sigwaitinfo() fetch the signal: %d\n", signum);
signal_handler(signum);
}
}
}
void ObSignalTest::run_test()
{
if (g_run_test) {
//first sigmask in main thread
sigset_t bset, oset;
sigemptyset(&bset);
sigaddset(&bset, SIGINT);
if (pthread_sigmask(SIG_BLOCK, &bset, &oset) != 0)
printf("!! Set pthread mask failed\n");
//second start signal deal thread
signal_deal_thread_.start();
//loop call localtime_r
time_t now;
struct tm ltime;
now = time(NULL);
while(1) {
localtime_r(&now, <ime);
}
signal_deal_thread_.wait();
}
}
TEST_F(ObSignalTest, signal_test)
{
run_test();
}
}
//use ./test_signal_handle run
int main(int argc, char **argv)
{
::testing::InitGoogleTest(&argc,argv);
if (argc >= 2) {
if (strcmp("run", argv[1]) ==0) {
::test::g_run_test = true;
}
}
return RUN_ALL_TESTS();
}