Linux异步信号处理函数引发的死锁及解决方法

简介:

死锁的发生

自己所在的团队在开发新版本过程中,一次测试环境发生了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();
}
相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
1月前
|
Linux
关于Linux目录访问函数总结
关于Linux目录访问函数总结
13 1
|
3天前
|
存储 算法 网络协议
【探索Linux】P.26(网络编程套接字基本概念—— socket编程接口 | socket编程接口相关函数详细介绍 )
【探索Linux】P.26(网络编程套接字基本概念—— socket编程接口 | socket编程接口相关函数详细介绍 )
11 0
|
4天前
|
算法 安全 Linux
【探索Linux】P.20(多线程 | 线程互斥 | 互斥锁 | 死锁 | 资源饥饿)
【探索Linux】P.20(多线程 | 线程互斥 | 互斥锁 | 死锁 | 资源饥饿)
11 0
|
4天前
|
存储 安全 Linux
【探索Linux】P.18(进程信号 —— 信号捕捉 | 信号处理 | sigaction() )
【探索Linux】P.18(进程信号 —— 信号捕捉 | 信号处理 | sigaction() )
6 0
|
4天前
|
消息中间件 Unix Linux
【探索Linux】P.14(进程间通信 | 匿名管道 | |进程池 | pipe() 函数 | mkfifo() 函数)
【探索Linux】P.14(进程间通信 | 匿名管道 | |进程池 | pipe() 函数 | mkfifo() 函数)
10 0
|
4天前
|
Linux 开发工具
linux中出现不在 sudoers 文件中。此事将被报告的解决方法
linux中出现不在 sudoers 文件中。此事将被报告的解决方法
12 0
|
4天前
|
Linux Shell
Linux中system函数
Linux中system函数
7 0
|
10天前
|
Linux Shell 调度
【Linux系列】fork( )函数原理与应用详解——了解【父子进程及其特性】(代码演示,画图帮助理解,思维导图,精简)(11)
【Linux系列】fork( )函数原理与应用详解——了解【父子进程及其特性】(代码演示,画图帮助理解,思维导图,精简)(11)
|
18天前
|
算法 Linux Shell
【linux进程(二)】如何创建子进程?--fork函数深度剖析
【linux进程(二)】如何创建子进程?--fork函数深度剖析
|
1月前
|
Linux 开发者
Linux文件编程(open read write close函数)
通过这些函数,开发者可以在Linux环境下进行文件的读取、写入和管理。 买CN2云服务器,免备案服务器,高防服务器,就选蓝易云。百度搜索:蓝易云
94 4