当前位置:首页 > > 程序员写个解
[导读]在我看来最不值得一提的BUG是那种可以重复复现的,他的稳定复现通常排查起来没啥技术含量, 早些年我处理一个不值得一提的BUG,BUG也很好复现,难点是复现时间固定在4小时左右,BUG由于文件资源未释放引起进程访问文件数目受限而崩溃,早期Android系统用该BUG获取到root权限, 本文向你分享,如何根据错误提示和参考手册找到故障点,指导新码农如何正确阅读Linux帮助手册(man page), 最后总结我的排查过程给小白一点实用的建议。好下面开始不如步入正题。需要调试的是一个监控程序,代码非常简单,2个线程执行不同的任务,每个任务都是间隔15秒执行一次,程序固定在大约4小时后崩溃。代码简单到用不着任何同步机制、没有任何通信,极少的内存访问,按理来说他就不应该存在BUG,然而还是发生了。

最不值得一提的BUG

在我看来最不值得一提的BUG是那种可以重复复现的,他的稳定复现通常排查起来没啥技术含量, 早些年我处理一个不值得一提的BUG,BUG也很好复现,难点是复现时间固定在4小时左右,BUG由于文件资源未释放引起进程访问文件数目受限而崩溃,早期Android系统用该BUG获取到root权限, 本文向你分享,如何根据错误提示和参考手册找到故障点,指导新码农如何正确阅读Linux帮助手册(man page), 最后总结我的排查过程给小白一点实用的建议。好下面开始不如步入正题。需要调试的是一个监控程序,代码非常简单,2个线程执行不同的任务,每个任务都是间隔15秒执行一次,程序固定在大约4小时后崩溃。代码简单到用不着任何同步机制、没有任何通信,极少的内存访问,按理来说他就不应该存在BUG,然而还是发生了。

第1个4小时:缩小排查范围,是什么引起段错误

在源码若干位置加上打印执行的函数、行号, 打开调试选项重新编译应用程序,开启coredump选项,耐心等待4小时后故障复现。gdb打开coredump 确认段错误(Segmentation fault),栈溯确认崩溃现场调用栈。段错误位于ti_ck_mutil函数第266行之后。
TickStatusIO():105ti_ck_mutil():266Segmentation fault (core dumped) (gdb) bt#0  0x401b28e0 in vfwprintf () from /lib/libc.so.6#1  0x00009d10 in ti_ck_mutil (cmdstr=0xbebffa4c, len=1) at src/ti.c:268#2  0x00008e2c in TickStatusIO () at src/initgpio.c:106#3  0x00009238 in main (argc=1, argv=0xbebffbf4) at src/initgpio.c:304

审查ti_ck_mutil函数内226行之后的代码,结合栈底位置是vfwprintf函数入口,基本可以确定导致崩溃位置是fread函数,fread可能会有什么错误呢?
int ti_ck_mutil(char *cmdstr, int count){ FILE *stream; char strout[256]; int ret, failcount = 0;  for (int i = 0; i < count; i++) { printf("%s()%d\n", __FUNCTION__, __LINE__);//226行 stream = popen(cmdstr, "r");//未检查文件是否成功 ret = fread(strout, sizeof(char), sizeof(strout), stream); // 228行  strout[ret] = '\0'; pclose(stream); // ... } return failcount;}
fread输入参数只有4个,猜测可能存在的失败原因有3点:
1、被编译器优化后strout的缓存不是256

但后面用的是算数表达式sizeof,就算被优化也不会造成错误。
观点:暂时不去瞎想2、fread写入最后一个字符时溢出。

strout后第256地址也被填写了,实际我读写的文件不超过64byte,不应该超过256。
即使第256地址被fread写了,相当于内存访问越接。访问越接发生什么错误都不奇怪,轻微越接会影响附近变量的值,比如ret和stream的值改变,大范围越界破坏调用栈。观点:猜测fread可能访问越限,但绝对没破坏调用栈。若破坏调用栈,那么栈不会是整整齐齐打印4个函数,而是输出若干问号(“?? ()”),找不到函数名称标签。
#0  0x000028e0 in ?? () #1  0x000038e8 in ?? () #2  0x000048ec in ?? () #3  0x000068e0 in ?? () #4  0x00009d10 in ti_ck_mutil (cmdstr=0xbebffa4c, len=1) at src/ti.c:268#5  0x00008e2c in TickStatusIO () at src/initgpio.c:106#6  0x00009238 in main (argc=1, argv=0xbebffbf4) at src/initgpio.c:304

3、stream文件描述符无效观点:有可能,源码未对popen返回结果做判断。

第2个4小时:是内存越界?还是资源不足?

于是结合猜测2和3,对源码做2处理修改:1、不向fread传递完整内存长度,保证最后一个字符不被fread填写 2、判断popen返回值
stream = popen(cmdstr, "r");ret = fread(strout, sizeof(char), sizeof(strout), stream); 修改后 stream = popen(cmdstr, "r");if (stream == 0) { perror("popen error:");}ret = fread(strout, sizeof(char), sizeof(strout) - 1, stream);

继续等待4小时,程序依旧崩溃,输出崩溃前提示执行popen失败,返回值0,错误原因记录在errno里,errno指示打开太多文件,资源不足。
	
popen error:: Too many open files

机理分析:为什么文件打开太多?

进一步定位到故障点在popen函数上,问题是:啥叫文件打开太多?查看popen帮助介绍:man popen。或许能给我解释
RETURN VALUEThe popen() function returns NULL if the fork(2) or pipe(2) calls fail, or if it cannot allocate memory.
本质上popen是个“壳",它返回0的原因有两个:1、它间接调用fork()创建子进程执行脚本,间接调用pipe()创建管道,子进程输出信息从管道传递到父进程。2、没有足够的内存分配。从第2点:没有足够的内存方向去排查,无非是内存泄漏咯,通常是申请内存有释放干净导致。c语言标准内存分配函数有malloc、calloc、realloc、reallocarray,对应的释放函数只有free。我应该在源码上搜索,是否所有“分配函数和释放函数都一一配对”,哦~别忘了,小白可能还不清楚,除了常用的malloc外,还有像mmap这样的内存分配函数,它有专用的释放函数munmap从搜索结果上看,数目是能对得上的,暂且粗略的判定不存内存泄漏。更仔细的排查方向应该是:确定代码执行流真的执行到释放函数,而不是单纯地看数目是否匹配



在继续阅读popen的errors段落描述。
ERRORSThe popen() function does not set errno if memory allocation fails. If the underlying fork(2) or pipe(2) fails, errno is set appropriately. If the type argument is invalid, and this condition is detected, errno is set to EINVAL.
popen不会因为内存分配失败而在errno记录错误码,如果是fork()或pipe()函数执行失败则在errno设置相应错误码。忙半天忙个寂寞,年轻人,别学会写一、二、三,就自以为无师自通懂得写四、写一万。读完man全文再入手好不好!既然errno提示具体错误信息,就不可能是内存泄漏,执行失败原因一定是Too many open files的字面意思。回想以前初学Linux时有个知识点:为了防止某用户打开过多的文件,系统对进程件访问数目有限制,默认是10242016年4月参加宋宝华的线下培训,他说Android刚出来时有个提权的方法(root权限):创建1024个无用子进程资源且不释放,第1025个进程就能得到root权限命令查看应用程序运行一段时间后,有多少文件描述符号(file descriptor)没有释放。果然,每间隔15秒文件描述符就多一个。256分钟后达到1024个文件描述符,时间上和软件4小时崩溃很接近。
watch -n 1 ls -l /proc/PID/fd

再用之前的筛选方法:排查open和close的函数是一一匹配。发现open关键词筛选出
6行,close作为关键词筛出5行。opendir没有对应的close。


捂脸!!!“Linux下一切皆是文件”我还没理解透彻,没意识到打开目录(opendir)也是文件资源,应用程序某线程每间隔15秒就访问一次目录。man opendir确认closedir是它的配对关闭函数。
SEE ALSOopen(2), closedir(3), dirfd(3), readdir(3), rewinddir(3), scandir(3), seekdir(3), telldir(3)
添加上closedir后故障得以修复。

顺带提一下

贴图用的搜索工具不是grep而是我自己写的脚本jgrep,它的用法和grep完全一样,输入前面的数字能打开对于文件所在行,对于搜索源码、系统配置文件检索、跳转特别适用。如果你对jgrep感兴趣的话,在我的公众号“程序员写个解”发送 “20220411” 可获取。




总结建议

BUG成功得以修复,它本是不应该犯的错误,在这里我给自己和读者建议:1、以后使用不熟悉的API,首先查阅他的帮助手册2、对于内存分配函数有相互独立的API,比如malloc对应free、mmap对应munmap。跟着手册建议的API去掉释放资源,避免不可预知的故障发生。最后,如果你觉得文章对你有所帮助,有启发作用。欢迎点击,把今天的内容分享给你的好友,和他一起讨论学习。
本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: 驱动电源

在工业自动化蓬勃发展的当下,工业电机作为核心动力设备,其驱动电源的性能直接关系到整个系统的稳定性和可靠性。其中,反电动势抑制与过流保护是驱动电源设计中至关重要的两个环节,集成化方案的设计成为提升电机驱动性能的关键。

关键字: 工业电机 驱动电源

LED 驱动电源作为 LED 照明系统的 “心脏”,其稳定性直接决定了整个照明设备的使用寿命。然而,在实际应用中,LED 驱动电源易损坏的问题却十分常见,不仅增加了维护成本,还影响了用户体验。要解决这一问题,需从设计、生...

关键字: 驱动电源 照明系统 散热

根据LED驱动电源的公式,电感内电流波动大小和电感值成反比,输出纹波和输出电容值成反比。所以加大电感值和输出电容值可以减小纹波。

关键字: LED 设计 驱动电源

电动汽车(EV)作为新能源汽车的重要代表,正逐渐成为全球汽车产业的重要发展方向。电动汽车的核心技术之一是电机驱动控制系统,而绝缘栅双极型晶体管(IGBT)作为电机驱动系统中的关键元件,其性能直接影响到电动汽车的动力性能和...

关键字: 电动汽车 新能源 驱动电源

在现代城市建设中,街道及停车场照明作为基础设施的重要组成部分,其质量和效率直接关系到城市的公共安全、居民生活质量和能源利用效率。随着科技的进步,高亮度白光发光二极管(LED)因其独特的优势逐渐取代传统光源,成为大功率区域...

关键字: 发光二极管 驱动电源 LED

LED通用照明设计工程师会遇到许多挑战,如功率密度、功率因数校正(PFC)、空间受限和可靠性等。

关键字: LED 驱动电源 功率因数校正

在LED照明技术日益普及的今天,LED驱动电源的电磁干扰(EMI)问题成为了一个不可忽视的挑战。电磁干扰不仅会影响LED灯具的正常工作,还可能对周围电子设备造成不利影响,甚至引发系统故障。因此,采取有效的硬件措施来解决L...

关键字: LED照明技术 电磁干扰 驱动电源

开关电源具有效率高的特性,而且开关电源的变压器体积比串联稳压型电源的要小得多,电源电路比较整洁,整机重量也有所下降,所以,现在的LED驱动电源

关键字: LED 驱动电源 开关电源

LED驱动电源是把电源供应转换为特定的电压电流以驱动LED发光的电压转换器,通常情况下:LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: LED 隧道灯 驱动电源
关闭