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

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介:

死锁的发生

自己所在的团队在开发新版本过程中,一次测试环境发生了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日志并进行多维度分析。
目录
相关文章
|
23天前
|
Linux
【Linux】System V信号量详解以及semget()、semctl()和semop()函数讲解
System V信号量的概念及其在Linux中的使用,包括 `semget()`、`semctl()`和 `semop()`函数的具体使用方法。通过实际代码示例,演示了如何创建、初始化和使用信号量进行进程间同步。掌握这些知识,可以有效解决多进程编程中的同步问题,提高程序的可靠性和稳定性。
68 19
|
25天前
|
Linux Android开发 开发者
linux m、mm、mmm函数和make的区别
通过理解和合理使用这些命令,可以更高效地进行项目构建和管理,特别是在复杂的 Android 开发环境中。
61 18
|
5月前
|
Linux
Linux下使用ls查看文件颜色全部为白色的解决方法,以及Linux中文件颜色介绍
Linux下使用ls查看文件颜色全部为白色的解决方法,以及Linux中文件颜色介绍
230 2
|
1月前
|
存储 监控 Linux
嵌入式Linux系统编程 — 5.3 times、clock函数获取进程时间
在嵌入式Linux系统编程中,`times`和 `clock`函数是获取进程时间的两个重要工具。`times`函数提供了更详细的进程和子进程时间信息,而 `clock`函数则提供了更简单的处理器时间获取方法。根据具体需求选择合适的函数,可以更有效地进行性能分析和资源管理。通过本文的介绍,希望能帮助您更好地理解和使用这两个函数,提高嵌入式系统编程的效率和效果。
107 13
|
3月前
|
安全 Linux 数据安全/隐私保护
Linux 忘记密码解决方法
Linux 忘记密码解决方法
66 2
Linux 忘记密码解决方法
|
4月前
|
Linux Shell
Linux系统编程:掌握popen函数的使用
记得在使用完 `popen`打开的流后,总是使用 `pclose`来正确关闭它,并回收资源。这种做法符合良好的编程习惯,有助于保持程序的健壮性和稳定性。
203 6
|
4月前
|
Linux Shell
Linux系统编程:掌握popen函数的使用
记得在使用完 `popen`打开的流后,总是使用 `pclose`来正确关闭它,并回收资源。这种做法符合良好的编程习惯,有助于保持程序的健壮性和稳定性。
211 3
|
4月前
|
Linux
在Linux内核中根据函数指针输出函数名称
在Linux内核中根据函数指针输出函数名称
|
5月前
|
Linux PHP
Linux CentOS 宝塔 Suhosin禁用php5.6版本eval函数详细图文教程
【8月更文挑战第27天】本文介绍两种禁用PHP执行的方法:使用`PHP_diseval_extension`禁用和通过`suhosin`禁用。由于`suhosin`不支持PHP8,仅适用于PHP7及以下版本,若服务器安装了PHP5.6,则需对应安装`suhosin-0.9.38`版本。文章提供了详细的安装步骤,并强调了宝塔环境下与普通环境下的PHP路径差异。安装完成后,在`php.ini`中添加`suhosin.so`扩展并设置`executor.disable_eval = on`以禁用执行功能。最后通过测试代码验证是否成功禁用,并重启`php-fpm`服务生效。
85 2
|
5月前
|
Shell Linux C语言
Linux0.11 execve函数(六)
Linux0.11 execve函数(六)
111 1