记一个openwrt reboot异步信号处理死锁问题
写在前面
觉得本页面排版单调的话,可以尝试到这里看。
问题背景
在 openwrt
上碰到了一个偶现的 reboot
失效问题。执行 reboot
之后系统并没有重启,此时控制台还能工作。
初步排查
首先复现问题,发现复现后控制台仍可正常运行,但此时重复执行 reboot
也无效,执行 reboot -f
则可正常触发重启。
此处 reboot
是一个指向 busybox
的软链接,从 help
信息
-f Force (don't go through init)
中可以看出 reboot
和 reboot -f
的区别在于 reboot
会先通知 init
进程进行一系列操作,而 reboot -f
则直接调内核。
看下 busybox
源码, 如果带了 -f
则直接调用 C 库的 reboot 函数
,如果没有带 -f
参数,则只会通过 kill
发信号给 1号进程
。
if (!(flags & 4)) { /* no -f */
//TODO: I tend to think that signalling linuxrc is wrong
// pity original author didn't comment on it...if (ENABLE_LINUXRC) {/* talk to linuxrc *//* bbox init/linuxrc assumed */pid_t *pidlist = find_pid_by_name("linuxrc");if (pidlist[0] > 0)rc = kill(pidlist[0], signals[which]);if (ENABLE_FEATURE_CLEAN_UP)free(pidlist);}if (rc) {/* talk to init */if (!ENABLE_FEATURE_CALL_TELINIT) {/* bbox init assumed */rc = kill(1, signals[which]);if (init_was_not_there())rc = kill(1, signals[which]);} else {/* SysV style init assumed *//* runlevels:* 0 == shutdown* 6 == reboot */execlp(CONFIG_TELINIT_PATH,CONFIG_TELINIT_PATH,which == 2 ? "6" : "0",(char *)NULL);bb_perror_msg_and_die("can't execute '%s'",CONFIG_TELINIT_PATH);}}} else {rc = reboot(magic[which]);}
目前 reboot -f
正常,那问题就出在用户空间调用 reboot()
之前的操作中了。
现场分析
既然知道了 reboot
是通过发送信号给 init
进程,那么下一步自然就是搞清楚 init
进程为什么卡住了。
出问题时控制台还能用,这是个好消息。先通过 ps
列出进程信息看下,发现 procd
处于 S
状态。
S interruptible sleep (waiting for an event to complete)`
但只知道这个没太大作用,我们需要更多信息,幸好 linux
还有 proc
文件系统
/proc 文件系统是一个虚拟文件系统, 最初开发 /proc 文件系统是为了提供有关系统中进程的信息。但是由于这个文件系统非常有用,因此内核中的很多元素也开始使用它来报告信息,或启用动态运行时配置。
知道了某个进程的 pid
号。就可以在 /proc/<pid>
目录下,获取到大量的进程相关信息。例如 cat /proc/1/status
查看状态信息 , cat /proc/1/stack
查看栈信息。
$ cat /proc/1/stack[<ffffff800808526c>] __switch_to+0x90/0xc4[<ffffff80080f78c4>] futex_wait_queue_me+0xb8/0x108[<ffffff80080f8018>] futex_wait+0xcc/0x1b4[<ffffff80080f9728>] do_futex+0xdc/0x940[<ffffff80080fa0c8>] SyS_futex+0x13c/0x148[<ffffff800808325c>] __sys_trace+0x4c/0x4c[<ffffffffffffffff>] 0xffffffffffffffff
从栈信息看,似乎在等待某个锁。
跟踪工具
情况又清晰了一点,但还不够,下一步用跟踪工具看下。
先上 strace
, strace
是跟踪进程行为的利器, 可以直接用 strace
来启动一个程序,从头开始跟踪,例如 strace reboot
,也可以在程序运行过程中,通过指定 pid
动态 attach
上去,中途开始跟踪,例如目前这种情况,在 reboot
之前先运行 strace -p 1
,即可观察卡住前 1号进程
都执行了什么操作。
从 strace
的输出,加上我自己增加的一些 log
验证,此时已经锁定到问题出在一个打印语句中,展开后是对 vsyslog
的调用。init
就卡在这个调用中,一去不复返。
如果有 gdb
那就更简单了,直接在卡住后连上去,看下 backtrace
,不仅能直接看到 init
调用了 vsyslog
,还能进一步看到是 glibc
内部在 vsyslog
中又调用了 realloc
,最终卡住。log
如下(本机的一些路径信息用 *** 代替了)
(gdb) bt#0 0x0000007f8f5948e0 in __lll_lock_wait_private () from /lib/libc.so.6#1 0x0000007f8f543420 in realloc () from /lib/libc.so.6#2 0x0000007f8f539108 in _IO_mem_finish () from /lib/libc.so.6#3 0x0000007f8f5316c8 in fclose@@GLIBC_2.17 () from /lib/libc.so.6#4 0x0000007f8f586d94 in __vsyslog_chk () from /lib/libc.so.6#5 0x0000007f8f6a727c in vsyslog (__ap=..., __fmt=0x40c98c "- shutdown -\n",__pri=6)at /***-glibc/toolchain/include/bits/syslog.h:47#6 ulog_syslog (ap=..., fmt=0x40c98c "- shutdown -\n", priority=6)at /***/compile_dir/target/libubox-2016-02-26/ulog.c:117#7 ulog (priority=priority@entry=6, fmt=fmt@entry=0x40c98c "- shutdown -\n")at /***/compile_dir/target/libubox-2016-02-26/ulog.c:172#8 0x0000000000404c84 in state_enter ()at /***/compile_dir/target/procd-2016-02-08/state.c:155#9 0x0000000000404314 in signal_shutdown (signal=<optimized out>,siginfo=<optimized out>, data=<optimized out>)at /***/compile_dir/target/procd-2016-02-08/signal.c:61#10 <signal handler called>---Type <return> to continue, or q <return> to quit---#11 0x0000007f8f565070 in fork () from /lib/libc.so.6#12 0x000000000040b19c in queue_next ()at /***/compile_dir/target/procd-2016-02-08/plug/hotplug.c:335#13 0x0000007f8f6a3ce0 in uloop_handle_processes ()at /***/compile_dir/target/libubox-2016-02-26/uloop.c:545#14 uloop_run ()at /***/compile_dir/target/libubox-2016-02-26/uloop.c:685#15 0x0000000000404074 in main (argc=1, argv=0x7fdf7255c8)at /***/compile_dir/target/procd-2016-02-08/procd.c:75
分析原因
找到了卡住的点,搜索一番,问题的原因也就很明显了。这是一个异步信号安全问题。
前面说到 reboot
时是发送了一个信号给 1号进程
, 而 1号进程procd
的这段出问题代码,正是在信号处理函数中被调用的。
搜下 信号处理 死锁
之类的关键词,就可以搜到很多人前仆后继地踩了这个坑。信号的到来会打断正常的执行流程,转而执行异步信号处理函数,由于不确定被打断的位置,所以异步信号处理函数的编写是很有讲究的,只能调用异步信号安全的函数。可以在 man 7 signal
中找到这个异步信号安全函数的列表。太占篇幅这里就不列了。
除了这些函数,其他的调用都不保证是安全的。本例中是调用了syslog
, 里面执行了内存分配操作。此时如果信号发生时正常流程中也在执行内存分配操作,那就可能发生死锁,因为 glibc
中的内存分配操作是有锁的,正常流程中上锁之后被信号打断,信号处理函数中又去拿这个锁,就死锁了。
此处要区分好 线程安全
和 异步信号安全
。例如
lock
do something
unlock
有锁保护之后,多线程调用这段代码,任意时刻只有一个线程可拿到锁,就保证只会有一个线程在执行中间的 do something
,但当某个线程拿到锁后正在执行 do something
时,是可以被信号打断的。如果信号处理函数中,也尝试执行这段函数,那么信号处理函数就会卡在 lock
上一直拿不到锁。
回到问题本身,这个问题的直接原因是信号处理函数中调用了 LOG
,而展开后调用了不安全的 vsyslog
。
但解决问题不能只是简单地注释掉这行,这样治标不治本,因为这个信号处理函数中还调用了不少其他函数,都是有风险的。
要解决这个问题,还得完全按标准来,保证信号处理函数中只调用异步信号安全的函数,才能永绝后患。
方案一
为了满足异步信号安全,在信号处理函数中编程就难免限制多多,束手束脚,申请个内存,加个打印,都有可能死锁。
一个常用的方式是将异步信号处理改成同步信号处理。思路就是将信号屏蔽掉,专门开一个线程开处理信号。
可以参考 Linux 多线程应用中如何编写安全的信号处理函数
这里贴下 man pthread_sigmask
中的例子,主线程中先屏蔽一些信号,然后创建了一个特定的线程,通过 sigwait
来检测处理这些信号。如此一来处理信号就是在正常的上下文中完成的,不必考虑线程安全问题。
EXAMPLEThe program below blocks some signals in the main thread, and then creates a dedicated thread to fetch those signals via sigwait(3).The following shell session demonstrates its use:$ ./a.out &[1] 5423$ kill -QUIT %1Signal handling thread got signal 3$ kill -USR1 %1Signal handling thread got signal 10$ kill -TERM %1[1]+ Terminated ./a.outProgram source#include <pthread.h>#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <signal.h>#include <errno.h>/* Simple error handling functions */#define handle_error_en(en, msg) \do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)/* 信号处理线程 */static void *sig_thread(void *arg){sigset_t *set = arg;int s, sig;for (;;) {s = sigwait(set, &sig); /* 主动等待指定的信号集 */if (s != 0)handle_error_en(s, "sigwait");printf("Signal handling thread got signal %d\n", sig); /* 进行信号处理,此时不必局限于调用异步信号安全的函数 */}}intmain(int argc, char *argv[]){pthread_t thread;sigset_t set;int s;/* Block SIGQUIT and SIGUSR1; other threads created by main()will inherit a copy of the signal mask. */sigemptyset(&set); /* 创建一个空信号集 */sigaddset(&set, SIGQUIT); /* 将SIGQUIT加入信号集 */sigaddset(&set, SIGUSR1); /* 将SIGUSR1加入信号集 */s = pthread_sigmask(SIG_BLOCK, &set, NULL); /* 屏蔽信号集,屏蔽后内核收到这些信号,不会触发任何异步的信号处理函数,只是登记下来 */if (s != 0)handle_error_en(s, "pthread_sigmask");s = pthread_create(&thread, NULL, &sig_thread, (void *) &set); /* 创建信号处理线程,传入屏蔽的信号集,也就是要同步处理的信号集 */if (s != 0)handle_error_en(s, "pthread_create");/* Main thread carries on to create other threads and/or doother work */pause(); /* Dummy pause so we can test program */}
了解了这种同步信号处理模型,那目前的问题能否套用呢 ? 很遗憾不行,因为这种方式需要屏蔽信号,而信号的屏蔽是会被 fork
继承的,回到问题本身,这次的主角是 1号进程procd
,整个用户空间的其他进程全是它的子进程,牵一发而动全身,信号屏蔽还是暂不考虑了。
方案二
既然不能屏蔽信号,那异步信号处理函数就还是存在。可以考虑把原来的信号处理函数做到事情挪出来,放到独立的一个线程中去做,异步信号处理函数只负责通知下这个线程干活。
怎么通知呢? man 7 signal
看看有什么异步信号安全的函数可以用,看起来 sim_post
似乎不错。
首先初始化一个 semaphore
, 然后在信号处理线程中调用 sem_wait
, 等到后执行实际的信号处理 , 而在异步信号处理函数中仅调用 sem_post
,起到通知的作用。
这个方案的问题在于引入了多线程。本来 procd
是单线程的,其中用到的 uloop
等也并未考虑多线程下的线程安全,因此这里是有风险的,搞不好解 bug
就变成写 bug
了。
方案三
方案二的思路是没问题的,异步信号处理函数中只做最简单的事情,安全可靠,实际上的复杂操作留给正常的线程处理。
如果要避免多线程,那就得想办法在主线程中加入对信号的等待和处理,然后只在信号处理函数中进行简单操作,触发主线程处理。
具体的实现就多种多样了,例如最简单的,信号处理函数中将信号记录到全局变量中,主线程轮询。但轮询消耗资源呀,所以更好的做法是主线程阻塞在某个操作上,在信号到来打断这个阻塞操作后进行处理。
对于 procd
,其循环是使用的 uloop
,而 uloop
中会使用 epoll
监控指定的 fd
,并调用回调函数。
看看信号安全函数列表,read
和 write
都是异步信号安全的函数,由此我们可以开一个 pipe
或者 socket
,一端由异步信号处理函数写入,另一端由工作在正常进程上下文中的回调函数读出并处理。
最终我们使用了方案三,具体的是使用了管道,并直接复用了 openwrt
的 ustream
,这里展开就得涉及到 procd init
的工作流程分析了,后续有机会再写吧。
有一点可以提下,方案一和二用在 procd
中还有一个问题,就是不能跟原有的 uloop
中的 epoll
顺畅配合,会导致 reboot
要做的事情堆积在队列中却触发不了处理,需要等其他事件来打断这个 epoll
, 而方案三则没有这个问题。这也是 procd
和 uloop
的实现导致的,暂不展开。
其他
信号的细节还是蛮多的,例如同一信号多次发生会怎样,多个阻塞信号的到达顺序,进程级别的屏蔽处理和线程级别的屏蔽处理的差异,fork
和 exec
时的行为等。
异步信号同步化的方式,也有很多文章阐述,例如 signalfd
等本文都没提及。
说回 procd
,为什么原生的实现可以这么任性,直接在信号处理函数中调用非异步信号安全的函数呢? 这可能是 openwrt
默认 C库
是用的 musl
的原因吧
记一个openwrt reboot异步信号处理死锁问题相关推荐
- 多线程,并发,异步,死锁
线程:线程是进程的一个执行单元,线程是被系统独立调度和分派的基本单元,多线程技术在于提高CPU的利用率. 并发:并发执行不是同时执行CPU,任意时刻还是只能有一个线程能够占用CPU,只不过多个线程之间 ...
- FPGA中亚稳态、异步信号处理、建立和保持时间违例及题目合集
文章目录 一.亚稳态 1.1 降低亚稳态方法 二.异步信号处理的方式 三.建立和保持时间公式推导 3.1 建立时间 3.1 建立时间违例解决方法 3.2 保持时间违例解决方法 四.题目 一.亚稳态 亚 ...
- MySQL 遇到的死锁问_一个罕见的MySQL redo死锁问题排查及解决过程
原标题:一个罕见的MySQL redo死锁问题排查及解决过程 作者:张青林,腾讯云布道师.MySQL架构师,隶属腾讯TEG-基础架构部-CDB内核开发团队,专注于MySQL内核研发&相关架构工 ...
- 记一个自认为写得有点复杂的sql语句
记一个自认为写得有点复杂的sql语句,含义是跨3张表的select: select table_name,column_name,data_type,data_length,data_scale fr ...
- Disruptor是一个高性能的异步处理框架
为什么80%的码农都做不了架构师?>>> Disruptor是一个高性能的异步处理框架,或者可以认为是最快的消息框架(轻量的JMS),也可以认为是一个观察者模式实现,或者事件- ...
- 记一个简单的保护if 的sh脚本
记一个简单的保护if 的sh脚本 真是坑爹,就下面的sh,竟然也写了很久! if [ `pwd` != '/usr/xx/bin/tomcat' ] thenecho "rstall is ...
- 记一个 DataBindings遇到的坑,当ComboBox同时绑定DataSource与DataBindings的时候,绑定的元素一定要同名...
记一个 DataBindings遇到的坑,当ComboBox同时绑定DataSource与DataBindings的时候,绑定的元素一定要同名 原文:记一个 DataBindings遇到的坑,当Com ...
- 优雅地实现一个高效、异步数据实时刷新的列表
今日科技快讯 2月11日消息,据CNBC报道,当特斯拉公司于2019年1月宣布第二轮裁员以控制成本时,一个关键部门受到的打击尤为沉重.两名被裁汰的员工表示,负责向北美地区客户交付Model 3电动汽车 ...
- 记一个简单Android图书阅读器的制作过程
记一个简单图书阅读器的制作过程 微澜 2018/9/27 qq:9611153 从有个想法,到到一个可用程序,断断续续几个月,花上不少的功夫,即便是简单的程序一个人写下来也是很难的.越写越是发现,想要 ...
最新文章
- Linux(CentOS 7_x64位)系统下安装RDkit
- java if else 过多_Java中if-else过多怎么解决
- hunnu---11547 你的组合数学学得如何?
- Python缩进问题
- db2和mysql性能优化_DB2数据库性能调优的十个办法
- Coding Interview Guide -- 向有序的环形单链表中插入新节点
- HTML基本标签详解与运行截图
- 更快更高更强大,这是英特尔AI助力长城修缮的新进展
- 使用原生js实现邮箱模糊查询的效果
- tf入门-池化函数 tf.nn.max_pool 的介绍
- 电脑突然出现成功连接网络但不能上网、网络受限(解决办法)
- arduino步进电机程序库_arduino控制步进电机的库(带有驱动器)
- 制作Win10PE启动盘
- 一、为何我决定写Spring Cloud专栏
- ios持续化集成-fastlane+jenkins+蒲公英+alfred+Webhook通知企业微信
- JavaScript系列(一):浏览器及内核介绍
- free掉结点一定会造成断链吗?
- 如何给边框添加阴影效果
- 关于龙蜥社区20个问题 |龙蜥问答第1期
- Chap.17 总结《CL: An Introduction》 (Vyvyan Evans)
热门文章
- c语言中语句开始的标志是,第一个单片机程序(C语言编写)
- 印象笔记服务器自动备份,印象笔记跨平台自动备份短信图文教程
- 科学计算与数学建模-线性方程组求解的迭代法 思维导图
- Fractal Streets (POJ3889)(分形图、递归)
- WEB攻防-通用漏洞SQL读写注入ACCESS偏移注入MYSQLMSSQLPostgreSQL
- VMware虚拟化- vSphere vCenter HA理论与应用
- java 日历计算农历和节假日的常用类(包括除夕的算法)
- Day.js格式化时间
- 用d2rq转换MySQL为RDF数据
- Quartus II 13.1的安装与注册