记一次DFB异常问题处理和思考

  本周处理的诸多问题中有两个是串联起来的,都是DFB相关的问题,一个是FD Leak,一个是创建线程没退出,这两个问题的处理过程本不是很复杂,常规手段即可处理,但这次debug过程中,暴露出一些coding上的问题,也是一个提醒。


  这次问题我也初步总结FD leak的常规处理手段,可直接查看FD leak debug流程

问题处理过程

  此次问题环境是HDMI信源下做timming切换压测,每14s一次,timming切换之间会断掉信号然后重送信号,后台有进程在信号变化时截图,压测约5到6小时出现一次异常,产生coredump trace类似如下,当然这个是构造的,避免资安问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#0 0xf0402d5c in raise () from /lib/libc.so.6
(gdb) bt
#0 0xf0402d5c in raise () from /lib/libc.so.6
#1 0xf0406838 in abort () from /lib/libc.so.6
#2 0xf0d60c04 in __android_log_assert () from /mslib/libXXXXX.so
#3 0xf0d58310 in android::RefBase::decStrong(void const*) const ()from /mslib/libXXXXX.so
#4 0x0b10ec78 in XxXXXService::connect(android::sp<IXxXXXClient> const&) ()
.......
#10 0x0afd8114 in XxResourceHandle::~XxResourceHandle() ()
#11 0x0afd8f88 in object_buffer<XxResourceHandle, 256>::~object_buffer() ()
#12 0x0afd8fd8 in object_count_manager<xx_atomic, object_buffer<XxResourceHandle, 256> >::~object_count_manager() ()
#13 0xe939b0c0 in __run_exit_handlers ()
#14 0xf040815c in exit () from /lib/libc.so.6
#15 0xf07bdf4c in dfb_core_destroy_handler () from /mslib/libdirectfb-1.4.so.0
#16 0xf0ef3184 in ?? () from /lib/libpthread.so.0
Cannot access memory at address 0x0
#17 0xf04a3028 in ?? () from /lib/libc.so.6
Cannot access memory at address 0x0
Backtrace stopped: previous frame identical to this frame (corrupt stack?)
(gdb) q

  按照大部分新手的习惯,解coredump出来,看到abort首先会去看那个位置的code,查引发abort的原因(以前的我也会)。但这一例中,如果按照这样的思路去查,就会陷入无底洞,查不到原因。后面你也会发现每次压测出来的trace的位置不断变化,由此可能会以为是踩内存、并发之类的问题,绕了弯路。若仔细一看,你会发现异常点是dfb_core_destroy_handler中调用了exit。
  在这一例中看到exit基本也就定位到问题点了,异常的原因是dfb_core_destroy_handler中调用了exit,exit退出时会去析构全局和静态对象,带出了其他问题。根据ISO 2003 C++ standard的要求,exit函数在退出时会调用相关的对象的析构函数来释放资源,具体定义如下。

  我们可以通过下面的demo程序体验标准中的规定,特别指出,例子中a2对象满足“Automatic objects are not destroyed as a result of calling exit()”,因为它是栈上分配的。由此可见,这问题的本质也就是查exit被call的原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include "stdio.h"
#include "stdlib.h"
using namespace std;
class demo
{
int num ;
public:
demo(int x): num(x)
{
}
~demo(void)
{
cout<<"exit in destrution"<<endl;
printf("Num[%d] exit in destrution\r\n", num);
}
};
demo a1(1);
int main(int argc, char **argv)
{
demo a2(2);
static demo a3(3);

exit(0) ;
cout<<"exit in main\r\n"<<endl;
return 0;
}

  通过开log压测一晚复现问题,我们看到DFB在初始化阶段,调用dlopen时failed,提示log为“Too many open files”。这个log就是典型的FD leak问题,这部分可以参考Android-Stability【Fdleak】: Android Fd泄漏问题分析的说明。这个问题也就是当前进程中open了过多的fd导致dlopen时failed了。

01-01 09:58:51.724 2045 2045 I XXXXX: –> /mslib/directfb-1.4-0/wm/libdirectfbwm_default.so: cannot open shared object file: Too many open files

  请DFB owner review相关code,确认dlopen、dlclose调用成对,不会出现leak的问题,也就需要从对应进程的角度上分析。当进程异常退出时,所有资源被释放掉,也就没法看到什么内容leak了。这也就要求我们每压测一段时间,抓取/proc/$PID/fd下的信息,然后对比初始的,看增长的文件是哪些。
  手动压测几次后,“ls -l /proc/$PID/fd | grep . -c”统计fd数量变化,发现每次切换timming后都会增长4个fd。通过比对timming切换前后/proc/$PID/fd下内容的变化,我们发现是pipe leak。由此我们可以通过lsof来确认是哪些进程线程在用这个pipe,用法类似如下,”lsof | grep $pipp_id”定位。Unfortunately,异常时发现只有当前进程用着这个pipe,这也就说明,大概率是本进程创建pipe,起了其他线程退出后,本进程没关掉。

  review code没发现不合理的pipe使用,于是通过strace查看是否有读写这个pipe的行为,来反推异常点。具体例子如下,我们先确认对应fd号,然后“strace -p $PID -e trace=open,close,rean,write”追读写行为。可惜事与愿违,没看到任何对新增pipe的读写操作,问题一度断线。不过仔细一想,异常时有调用DFBCreate的行为,而这部分通过DFB owner release so到工程中,我们看不到代码,故更应该从这里下手。通过实验确认如果没做这个行为就不会出现leak,也就是说可能是so中有地方创建线程,然后创建线程的地方没有close pipe导致leak,请owner review code中异常点,通过加log分析,确认是DFB中有地方逻辑异常导致问题的发生。

  至此,初测问题解决,但回头来看。异常发生时,只是因为fd leak导致DFB没法成功初始化,而我们实际只需要模块的抓图功能,不需要绘图功能,而DFB组件异常也不应该影响拉挂整个服务,即DFB异常时不应该通过exit退出。基于此改code,仅保留异常提示,然后返回异常值,上层做容错,增强系统稳定性。
  提供修改给客户压测,压测后,发现问题现象依旧,但这次异常没有coredump了。幸运的是有tombstone文件,发现是挂在pthread_setschedparam上,具体如下。这次我跟owner要到代码了,通过review code可以确认param非NULL,那异常就应该是thread参数了,owner怀疑是pthread_create failed了,导致thread为0继而出现异常。

int pthread_setschedparam(pthread_t thread, int policy,const struct sched_param *param);

1
2
3
4
5
6
7
8
9
10
11
12
13
334864-01-01 10:21:54.114  2049  2049 I XXXX:  /applications/bin/XXXX(_Z13signalHandleriP9siginfo_tPv+0xa4) 
334865-01-01 10:21:54.114 2049 2049 I XXXX: /lib/libc.so.6(__default_rt_sa_restorer_v2+0)
334866-01-01 10:21:54.114 2049 2049 I XXXX: /lib/libpthread.so.0(pthread_getschedparam+0x4)
334867-01-01 10:21:54.114 2049 2049 I XXXX: /mslib/libdirect-1.4.so.0(direct_thread_create+0x18d)
334868-01-01 10:21:54.114 2049 2049 I XXXX: /mslib/directfb-1.4-0/inputdrivers/libdirectfb_mstarir.so(+0x15b4)
334869-01-01 10:21:54.114 2049 2049 I XXXX: /mslib/libdirectfb-1.4.so.0(+0x9b22a)
334870-01-01 10:21:54.114 2049 2049 I XXXX: /mslib/libdirectfb-1.4.so.0(dfb_core_part_initialize+0xa5)
334871-01-01 10:21:54.114 2049 2049 I XXXX: /mslib/libdirectfb-1.4.so.0(+0x90204)
334872-01-01 10:21:54.114 2049 2049 I XXXX: /mslib/libfusion-1.4.so.0(fusion_arena_enter+0x173)
334873-01-01 10:21:54.114 2049 2049 I XXXX: /mslib/libdirectfb-1.4.so.0(dfb_core_create+0x26b)
334874-01-01 10:21:54.114 2049 2049 I XXXX: /mslib/libdirectfb-1.4.so.0(DirectFBCreate+0x21f)
334875-01-01 10:21:54.114 2049 2049 I XXXX: /applications/bin/XXXX(MI_ATOM_XXX_Init+0x27c)
334876-01-01 10:21:54.114 2049 2049 I XXXX: /applications/bin/XXXX(MI_IMPL_XXX_Init+0x2ec)

  通过加log压测,确认异常时pthread_create的返回值为EAGAIN (11),通过Linux Programmer’s Manual pthread_create一节可以看到对这个返回值的解析,也就是线程创建数超限了,因此执行pthread_create创建新线程时,就会异常退出。

EAGAIN Insufficient resources to create another thread.
EAGAIN A system-imposed limit on the number of threads was encountered.There are a number of limits that may trigger this error: the RLIMIT_NPROC soft resource limit (set via setrlimit(2)), which limits the number of processes and threads for a real user ID, was reached; the kernel’s system-wide limit on the number of processes and threads, /proc/sys/kernel/threads-max, was reached (see proc(5)); or the maximum number of PIDs, /proc/sys/kernel/pid_max, was reached (see proc(5)).

  手动复现问题,然后通过”busybox ps -T > ps_t_$CNT.txt”,查看系统内全部进程线程信息,记录压测过程中相关变化,经过几次操作,发现每次都有两个dfb线程创建后没退出。长此以往,压测一段时间就会出现线程超限问题。因线程有通过prctl(PR_SET_NAME, “ThreadName1”); 设置线程名,也就很快定位问题点,review code后修正异常逻辑,重新手动测试,问题修正。

FD leak debug手段

一. 定位leak的是什么

  1、确认异常的PID。异常发生时,我们常会看到“Too many open files”这样的log,根据log所在PID的,或者其他手段我们可以确认异常发生的进程。
  2、查看异常进程的FD信息。如果有复现手段,我们可以先执行”ls -la /proc/$PID/fd | grep . -c”,然后复现,统计异常操作前后FD数的变化情况,以此确认问题。确认异常后,就在异常操作前后分别”ls -la /proc/$PID/fd”,比较差异,获取leak的FD,如果有具体的设备名/文件名,我们可以直接全局搜索,查找是哪里open了,没释放。如果是匿名的管道和内存,那就需要进一步定位的。

二. 定位是哪里打开

  3、通过lsof大致定位异常发生情形。”lsof | grep -Ei $FD”,这个命令可以看到是哪些进程在用fd,尤其是pipe leak的分析,这个有一定的助益。本例中,大概定位是创建线程方没有close pipe。
  4、strace定位操作行为。通过步骤2,我们可以获取到泄漏的fd号,配合“strace -p $PID -e trace=open,close,rean,write”,我们可以明确这个泄漏的pipe做了些什么,助攻问题定位。

三. 确认异常逻辑并修正

  5、在有如上信息后,review code中相关flow,主要在于确认对应环境和情景下,为何会出现open没有close的行为。包括”anon_inode:[eventpoll]”和”/dev/ashmem”在内的诸多泄漏问题,都是对应场景下的考虑不周导致异常发生。各类leak的处理点难点也就在这里,定位的异常发生点,接下来要考虑的就是flow的问题了。

总结

  这个问题发生在DFB相关资源不断申请和释放,每用一次就申请一次释放一次,因此对某个行为的压测就变成对DFB稳定的压测,从而使得问题暴露。实际上,DFB的资源申请只需要开机后做一次,此后其他进程使用这个服务的资源时,直接调用即可,这样整个系统的稳定性就会提升。另外,在这次debug过程中,我们可以看到没有做好各种防御编程,导致异常发生并恶化。

  任何时候,我们都不应该相信其他函数返回的结果,也不能假设库函数的返回结果是正常,始终坚持在防御代价比较低的情况下对各种场景做异常检测和防御,这是对软件稳定性的负责任。