Linux 信号安全

2017-04-05 Wednesday

最近遇到比较奇葩的问题,在信号处理函数中,为了方便查看收到的是何信号,会打印相关的日志,不过也因此在连续收到信号时导致死锁。

这里简单排查下原因,以及如何进行规避。

检查当前调用栈

查看当前的调用栈。

----- 通过pstack查看

$ pstack PID

----- 通过gdb连接过去

$ gdb attach PID

(gdb) info thread 各线程的栈信息

(gdb) thread apply all backtrace 所有线程的栈信息,类似于pstack命令

(gdb) thread 5 切换到某个线程

(gdb) where 查看当前栈信息

#0  0x0000003d1a80d4c4 in __lll_lock_wait () from /lib64/libpthread.so.0

#1  0x0000003d1a808e1a in _L_lock_1034 () from /lib64/libpthread.so.0

#2  0x0000003d1a808cdc in pthread_mutex_lock () from /lib64/libpthread.so.0

#3  0x0000000000400a9b in func1 () at lock.cpp:18

#4  0x0000000000400ad7 in thread1 (arg=0x0) at lock.cpp:43

#5  0x0000003d1a80673d in start_thread () from /lib64/libpthread.so.0

#6  0x0000003d19cd40cd in clone () from /lib64/libc.so.6

(gdb) frame 3 切换到加锁API函数的上一层

(gdb) print your_mutex 查看锁信息

如果线程阻塞的栈不变,一般为 __lll_lock_wait() 或者 __lll_lock_wait_private() ,那么基本可以确定是由于发生了死锁导致。

示例

如下是一个可能发生死锁的示例程序。

#include

#include

#include

#include

void int_handler(int signum)

{

time_t tt;

char timestr[12];

struct tm timenow;

time(&tt);

localtime_r(&tt, &timenow);

strftime(timestr, sizeof(timestr), "%m%d-%H%M%S", &timenow);

printf("%s Got a int signal %d\n", timestr, signum);

}

void quit_handler(int signum)

{

printf("%ld Got a quit signal %d\n", time(NULL), signum);

}

int main()

{

time_t now;

struct tm ltime;

signal(SIGINT, int_handler);

signal(SIGQUIT, quit_handler);

now = time(NULL);

while(1)

localtime_r(&now, &ltime);

return 0;

}

可以连续发送多次信号进行测试,或者使用如下命令。

while true; do pid=`pidof your-program`; if [ -n "$pid" ]; then kill $pid; sleep 0.01; else sleep 1; fi; done

while true; do ./daemon/your-program; echo "start" ; done

localtime死锁

简单来说,对应的堆栈为。

#0 0x0000003f6d4f805e in __lll_lock_wait_private () from /lib64/libc.so.6

#1 0x0000003f6d49dcad in _L_lock_2164 () from /lib64/libc.so.6

#2 0x0000003f6d49da67 in __tz_convert () from /lib64/libc.so.6

源码解析

/* Return the `struct tm' representation of *T in local time,

using *TP to store the result. */

struct tm *

__localtime_r (const time_t *t, struct tm *tp)

{

return __tz_convert (t, 1, tp);

}

weak_alias (__localtime_r, localtime_r)

/* Return the `struct tm' representation of *T in local time. */

struct tm *

localtime (const time_t *t)

{

return __tz_convert (t, 1, &_tmbuf);

}

libc_hidden_def (localtime)

也就是说,无论 localtime() 还是 localtime_r() 都是调用 __tz_convert() 完成实际功能,该函数的实现在 time/tzset.c 文件中。

其中有一部分代码是通过 __libc_lock_lock (tzset_lock); 加锁后的处理,而该锁是通过 __libc_lock_define_initialized (static, tzset_lock) 定义的 static 全局变量。

localtime() 和 localtime_r() 的实现都通过加锁实现了访问,但是 localtime() 同时会使用一个全局变量,所以后者不是线程安全的。

但这两个函数都不是信号安全的,如果在信号处理函数中使用,就要考虑到死锁的情况。比如,程序调用 localtime_r(),加锁后信号发生,信号处理函数中也调用 localtime_r() 的话,会因为获取不到锁所以一直阻塞。

死锁场景

最常见的是,也就是上述的,在日志打印时间调用了 localtime() 函数,而在信号处理函数中同时会打印日志,那么就可能会出现这一问题。

如果使用的是多进程,各个 localtime() 的调用都是安全的,另外,还有一个场景,是在多线程中同时 fork() 子进程。

后面的场景中,因为变量是共享的,那么如果多线程 fork() 子进程,而此时的某个线程在该函数的加锁阶段,子进程以 COW 方式共享主进程的内存空间,所以对应 localtime() 的锁也是被占用的情况,那么就可能导致子进程一直阻塞。

解决方案

对于部分场景,如果我们对锁有控制权,那么就可以在调用 fork() 创建子进程前,通过 glibc 库提供的函数 pthead_atfork() 加解锁,达到一致状态。

#include int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));

简单来说,创建子进程前在父进程中会调用 prepare 函数;创建子进程成功后,父进程会调用 parent 而子进程会调用 child 。这样,可以在 prepare 中释放所有的锁,parent 中按需要进行加锁。

由于没有办法操作 localtime 使用的锁,所以上述方式行不通。这样,只能是选择折中的办法,例如日志可以通过定时更新时间缓存的方式执行。

关于 pthread_atfork()

当父进程有多线程时,子进程继承父进程所有的互斥量、读写锁和条件变量的状态,如果父进程中的线程占有锁 (任一线程),那么子进程同样占有这些锁,当尝试重新获取锁时会导致一直阻塞。

如果子进程马上调用 exec 类函数,老的地址空间被丢弃,所以锁的状态无关紧要;否则,就需要清除锁的状态。

#include

#include

#include

#include

#include

pthread_mutex_t mutex;

void *another(void *arg)

{

(void) arg;

printf("Sub-thread lock\n");

pthread_mutex_lock(&mutex);

printf("Sub-thread locking\n");

sleep(2);

pthread_mutex_unlock(&mutex);

printf("Sub-thread unlock\n");

return NULL;

}

void child()

{

pthread_mutex_unlock(&mutex);

}

int main()

{

pthread_t tid;

pthread_mutex_init(&mutex, NULL);

pthread_create(&tid, NULL, another, NULL);

sleep(1); /* Just ensure the thread got mutex */

pthread_atfork(NULL, NULL, child);

int pid = fork();

if(pid < 0) {

pthread_join(tid, NULL);

pthread_mutex_destroy(&mutex);

return 1;

} else if (pid == 0) { /* child */

printf("Sub-process lock\n");

pthread_mutex_lock(&mutex);

printf("Sub-process locking\n");

pthread_mutex_unlock(&mutex);

printf("Sub-process unlock\n");

exit(0);

} else {

wait(NULL);

}

pthread_join(tid, NULL);

pthread_mutex_destroy(&mutex);

printf("Main routine exit\n");

return 0;

}

为了解决上述的死锁问题,需要在 fork() 调用前加入 pthread_atfork() 对应的代码。

一般使用的方式是,在 prepare 中执行加锁,在 parent 和 child 中实现解锁,这样可以保证在进入子进程前已经获得了锁,而在子进程中释放锁。

测试发现,锁可以多次释放,因此可以在进入子进程时把锁都释放掉。

线程安全、信号安全

一般来说线程是操作系统调度的最小单元,进程是资源分配的最小单元;一个进程可以派生多个线程,这些线程独立运行共享进程资源,那么在使用共享资源时,就需要考虑避免竞争条件、死锁、互斥等。

线程安全 Thread-Safe

在多线程 (单线程不存在) 并发执行场景中,如果一个函数可以安全地被多个线程并发调用,可以说这个函数是线程安全的。也就是说,一个线程安全的函数允许任意地被任意的线程调用,其它开发只需要关注业务逻辑。

有时候很难判断一个是否线程安全,不过如果有如下几条,那么说明这个函数是线程不安全的:

函数中访问、分配全局变量和堆。

使用了其他线程不安全的函数或者变量。

因此在编写线程安全函数时,要注意两点:

减少对临界资源的依赖,尽量避免访问全局变量、静态变量或其它共享资源,如果必须要使用则需要添加互斥锁;

线程安全的函数所调用到的函数也应该是线程安全的,如果调用了非线程安全函数,同样需要加互斥锁保护。

可重入 Re-entrant

一个函数想要成为可重入的函数,必须满足下列要求:

不能使用静态或者全局的非常量数据

不能够返回地址给静态或者全局的非常量数据

函数使用的数据由调用者提供

不能够依赖于单一资源的锁

不能够调用非可重入的函数

Reentrant Function

A function whose effect, when called by two or more threads, is guaranteed to be as if

the threads each executed the function one after another in an undefined order, even if

the actual execution is interleaved.

Thread-Safe

A function that may be safely invoked concurrently by multiple threads. Each function

defined in the System Interfaces volume of IEEE Std 1003.1-2001 is thread-safe unless

explicitly stated otherwise. Examples are any "pure" function, a function which holds

a mutex locked while it is accessing static storage, or objects shared among threads.

Async-Signal-Safe Function

A function that may be invoked, without restriction, from signal-catching functions.

No function is async-signal-safe unless explicitly described as such.

简单来说:

Reentrant:

不使用全局变量;

不调用non-reentrant函数。

Thread-safe:

可以访问全局变量,不过需要加锁

每次调用它返回不同的结果也没关系

Async-Signal-Safe:

只有几个固定的函数是 signal-safe 的,可以通过 man 7 signal 查看;

使用了锁的一定不是信号安全的,除非屏蔽了信号;

可重入函数一定是线程安全的,也是异步信号安全。

Nginx、MySQL 都分别实现了一堆的格式化函数,如 ngx_vslprintf()、my_safe_snprintf(),同时 Nginx 中的时间是定时更新的。

总结

总结一下,这种有全局锁的函数都不是信号安全的,比如 localtime()、gmttime()、free()、malloc() 等,但是无法使用 pthread_atfork() 来清理,因此在多线程中使用 fork 需要谨慎。

关于信号安全的函数可以通过 man 7 signal 查看。

如果喜欢这里的文章,而且又不差钱的话,欢迎打赏个早餐 ^_^

支付宝打赏

微信打赏

linux信号使用场景,Linux 信号安全相关推荐

  1. 【B站视频笔记】linux 进程间通信(ipc)信号(软中断信号)signal库函数、可靠信号和不可靠信号、信号集sigprocmask(信号掩码、信号递达Delivery、信号未决Pending)

    [视频教程]Linux信号详解(可靠信号.不可靠信号.阻塞信号.信号处理函数) [博文]Linux信号 文章目录 背景 课程笔记 一.如何让程序在后台运行 1.加"&"符号 ...

  2. Linux信号实践(1) --Linux信号编程概述

    中断 中断是系统对于异步事件的响应, 进程执行代码的过程中可以随时被打断,然后去执行异常处理程序; 计算机系统的中断场景:中断源发出中断信号 -> CPU判断中断是否屏蔽屏蔽以及保护现场 -&g ...

  3. Linux - 第8节 - 进程信号

    目录 1.Linux信号的基本概念 1.1.生活角度的信号 1.2.技术应用角度的信号 1.3.查看系统定义的信号列表 1.4.信号的处理常见方式 2.信号产生的一般方式 2.1.通过终端按键产生信号 ...

  4. linux内核定义的常用信号6,Linux中的信号

    在 Linux 中,理解信号的概念是非常重要的.这是因为,信号被用于通过 Linux 命令行所做的一些常见活动中.例如,每当你按 Ctrl+C 组合键来从命令行终结一个命令的执行,你就使用了信号.每当 ...

  5. linux signal函数用法,linux信号机制之sigaction构造体浅析,signal 函数,信号捕捉.

    来自:http://hi.baidu.com/phenix_yw/blog/item/6eb4ca391d1479f23a87ce19.html 信号安装函数sigaction(int signum, ...

  6. Linux信号 五 信号挂起与信号掩码操作接口集

    A signal may be blocked, which means that it will not be delivered until it is later unblocked. Betw ...

  7. Linux信号 四 异步等待信号与同步等待信号接口

    信号的同步等待和异步等待区别就是信号处理函数的执行与否,异步等待是信号处理函数已经执行了,同步等待是信号处理函数还没有执行. 异步等待接口:pause() 和 sigsuspend() 1. paus ...

  8. linux进程中对信号的屏蔽,linux进程中的信号屏蔽

    在linux的进程中可以接收到各种的信号,并且如果你不对信号进行处理,linux中的进程就会采用默认的处理方式处理,比如ctrl-c的信号,进程对它的处理就是终止进程的执行. 在linux中,我们也可 ...

  9. 【Linux系统编程】Linux信号列表

    00. 目录 文章目录 00. 目录 01. Linux信号编号 02. 信号简介 03. 特殊信号 04. 附录 01. Linux信号编号 在 Linux 下,每个信号的名字都以字符 SIG 开头 ...

最新文章

  1. javascript十六进制数字和ASCII字符之间转换
  2. i服务器2008系统,Windows Server 2008多路径 I/O 概述
  3. 不打擦边球、不搞黑线路能不能挣钱, 开始我是忐忑的
  4. Sublime Text 快捷键
  5. python xmxl 无法启动_Python小白到老司机,快跟我上车!基础篇(三)
  6. 硬核!OSPF路由协议归纳大全~
  7. java1121123211234321_使用for 语句打印显示下列数字形式:n=4 1 1 2 1 1 2 ,使用for 语句打印显示下列数字形式:n=4...
  8. 设置Backup-masters Hbase中只有一个HMaster ,hmaster挂掉了,客户端还能连接hbase集群进行数据读写吗
  9. Kafka高性能相关
  10. spring 使用其他类protected方法_Java操作bean、属性、方法的使用工具类
  11. 基于JAVA+Spring+MYSQL的房屋出售系统
  12. flamingo源码分析(1) :单例模式
  13. C/C++操作注册表键值添加/查询/删除详解
  14. java项目-第33期基于SSM框架的图书管理系统【毕业设计】
  15. 人工智能视觉处理教程(包含源码)
  16. springSessionDemo
  17. 很全的zencart 模板修改
  18. 阿里云面经之实习hr面
  19. 关于focusableInTouchMode不生效的问题
  20. word to pdf

热门文章

  1. java重写compareTo()方法,比较对象的大小
  2. 串口通讯控制器实现之----发送模块
  3. 【Dubbo】Dubbo 多协议支持、服务监控的三种方式
  4. YOLOv7 训练报错:subprocess.CalledProcessError: Command ‘git tag‘ returned non-zero exit status 127
  5. 负载均衡技术(一)———负载均衡技术介绍
  6. 在web.xml中配置过滤器
  7. Unity(C#) 虚方法详解
  8. 升级版NanoDet-Plus来了 | 简单辅助模块加速训练收敛,精度大幅提升
  9. IOS 下 -webkit-overflow-scrolling 引发的 bug
  10. Mongod 基础知识 + 命令 + 配置文件