zoukankan      html  css  js  c++  java
  • 记录一次句柄泄漏的异常解决

     最近入职了一家生产机器人的公司,我们做的软件就是运行在这个机器人上的,机器人服务有个特点就是里面的软件要连续24小时不间断服务,所以要求app能够持续的运行服务。

    测试那边很久以前就记录过一个奇异的问题,就是机器人在使用一天以后,会莫名奇妙的闪退,java层有crashhandler,但是也抓不到日志,任何的日志记录工具也没有用。

    从这周三开始我就开始着手解决这个问题。

    为了尽快复现这种很偶然的闪退,我试了各种方案,拔网线、杀程序、制造cpu使用率极高,最后终于发现一个规律,拔掉安卓主板与底盘机器人的通信,十分钟左右就会复现突然闪退的问题,crashHandler没有日志记录。

    突然很兴奋,立刻脸上数据线在命令提示符里输入以下命令

    adb logcat -v time >d:log.txt
    

      把机器上的日志输出到了d盘下的log.txt,仔细分析这个文件后发现了一个ndk导致的崩溃:

    --------- beginning of crash
    
    08-15 17:10:24.937 F/libc    (27117): Fatal signal 13 (SIGPIPE), code 0 in tid 27117 (adb)
    
    08-15 17:10:25.040 I/DEBUG   (  192): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
    
    08-15 17:10:25.040 I/DEBUG   (  192): Build fingerprint: 'Android/rk3288/rk3288:5.1.1/LMY49F/server01051636:userdebug/test-keys'
    
    08-15 17:10:25.040 I/DEBUG   (  192): Revision: '0'
    
    08-15 17:10:25.041 I/DEBUG   (  192): ABI: 'arm'
    
    08-15 17:10:25.040 W/NativeCrashListener(  572): Couldn't find ProcessRecord for pid 27117
    
    08-15 17:10:25.024 I/debuggerd(  192): type=1400 audit(0.0:63001): avc: denied { create } for name="tombstone_08" scontext=u:r:debuggerd:s0 tcontext=u:object_r:system_data_file:s0 tclass=file permissive=1
    
    08-15 17:10:25.041 I/DEBUG   (  192): pid: 27117, tid: 27117, name: adb  >>> adb <<<
    
    08-15 17:10:25.041 E/DEBUG   (  192): AM write failure (32 / Broken pipe)
    
    08-15 17:10:25.041 I/DEBUG   (  192): signal 13 (SIGPIPE), code 0 (SI_USER), fault addr --------
    
    08-15 17:10:25.034 I/debuggerd(  192): type=1400 audit(0.0:63002): avc: denied { write } for path="/data/tombstones/tombstone_08" dev="mmcblk0p13" ino=310090 scontext=u:r:debuggerd:s0 tcontext=u:object_r:system_data_file:s0 tclass=file permissive=1
    

      安卓系统的adb导致的崩溃,我…… 这怎么可能?

    这绝对不可能,肯定是什么误会导致的,再查,重启app,不到十分钟后又复现了,再抓log

    *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
    Build fingerprint: 'Android/rk3288/rk3288:5.1.1/LMY49F/server01051636:userdebug/test-keys'
    Revision: '0'
    ABI: 'arm'
    pid: 6301, tid: 10006, name: RenderThread  >>> guide.yunji.com.guide <<<
    signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
    Abort message: 'Encountered EGL error 12299 EGL_BAD_NATIVE_WINDOW during rendering'
        r0 00000000  r1 00002716  r2 00000006  r3 00000000
        r4 81afcdd8  r5 00000006  r6 00000018  r7 0000010c
        r8 b8ef0f3c  r9 81afcd28  sl b6e2357d  fp 81afcdd0
        ip 00002716  sp 81afc7e8  lr b6e23db1  pc b6e4a534  cpsr 600f0010
        d0  ffffffffffffffff  d1  0000000000000000
    

      

    这次是RenderThread,安卓系统的渲染线程?这…… 难道是我们安卓版子的供应商刷的固件有问题?

    我们把日志以及data/tombstone/里的文件甩给了板子供应商,但是一个tombstone文件大概有6~7M,他们问你们自己分析了吗?你们先分析下再商量,我们看了好久也没查出什么问题,然后继续查问题,继续复现崩溃

    后面的崩溃的原因也都是ndk方面的,但是每次都不一样,这特么就神奇了,难道真的是板子有问题?毫无头绪

    后来冷静了一下,重新梳理头绪:为什么不拔网线就要一天多才崩溃,拔了网线十分钟内就会崩溃?我们与底盘的通信到底做了什么?然后问了在这个公司工作一年多的小姑娘才知道,安卓版子与底盘通信就需要建立一个socket连接,其他的交互没有。

    于是我把查问题的重点转移到了socket通信上,死盯socket通信的日志:

    08-15 17:18:46.024 E/ConnectHelper(27160): initSocketSub: Exceptionjava.net.SocketException: socket failed: EMFILE (Too many open files)
    
    08-15 17:18:46.024 W/System.err(27160): java.net.SocketException: socket failed: EMFILE (Too many open files)
    
    08-15 17:18:46.024 W/System.err(27160): 	at libcore.io.IoBridge.socket(IoBridge.java:623)
    
    08-15 17:18:46.024 W/System.err(27160): 	at java.net.PlainSocketImpl.create(PlainSocketImpl.java:198)
    
    08-15 17:18:46.024 W/System.err(27160): 	at java.net.Socket.checkOpenAndCreate(Socket.java:687)
    
    08-15 17:18:46.024 W/System.err(27160): 	at java.net.Socket.setKeepAlive(Socket.java:474)
    
    08-15 17:18:46.024 W/System.err(27160): 	at com.hotelrobot.common.nethub.ConnectHelper.initSocketSub(ConnectHelper.java:187)
    
    08-15 17:18:46.024 W/System.err(27160): 	at com.hotelrobot.common.nethub.ConnectHelper.initSocket(ConnectHelper.java:126)
    
    08-15 17:18:46.024 W/System.err(27160): 	at com.hotelrobot.common.nethub.ConnectHelper.access$100(ConnectHelper.java:20)
    
    08-15 17:18:46.024 W/System.err(27160): 	at com.hotelrobot.common.nethub.ConnectHelper$ConnectHelperHandler.handleMessage(ConnectHelper.java:117)
    
    08-15 17:18:46.025 W/System.err(27160): 	at android.os.Handler.dispatchMessage(Handler.java:102)
    
    08-15 17:18:46.025 W/System.err(27160): 	at android.os.Looper.loop(Looper.java:135)
    
    08-15 17:18:46.025 W/System.err(27160): 	at android.os.HandlerThread.run(HandlerThread.java:61)
    
    08-15 17:18:46.025 W/System.err(27160): Caused by: android.system.ErrnoException: socket failed: EMFILE (Too many open files)
    

      开始的时候socket连不上底盘都会报错,错误内容一般都是 EHOSTUNREACH (No route to host)

    而这次却是EMFILE (Too many open files)

    这是什么鬼,然后google了一把,这个东西叫做句柄泄露。

    在linux中一个文件、一个串口、一个socket、一个线程都可以是一个文件,而一个文件会占用一个句柄,linux中一个进程默认的句柄最大数值是1024,当超过这个数值,linux就会对当前的进程进行kill,而kill的对象可以是任意对象,所以会造成各种异常原因的崩溃,看到这里我就大概明白了为什么每次崩溃ndk的报错原因都不一样了,这跟板子厂商一点关系都没有啊,于是我跟厂商说了,我们的问题,你们清白了,哈哈哈。

    接着,问题找到了,是句柄泄露,但是对于句柄泄露,如何查找呢?

    可以使用linux下的 lsof命令,列出所有占用的句柄,

    使用方式如下

    adb shell
    
    su
    
    lsof
    

      网上好多解决这个问题的都没说要用root权限,但是我的机器上如果不输入su,就打不出来全部的句柄,我也不知道为什么,还有就是lsof这个命令,可能我的安卓板子有点残废,后面加如任何的 lsof  pid和grep都是无效的,每次都把全部的句柄打出来,后来google下才知道,android下的lsof命令就是残废的,好吧,没办法凑合用吧。

    通过这个命令我们可以看到自己程序中句柄的序号,第一次打上我就看到,哎呀妈呀,句柄数800多了,然后发现不久后,app就闪退了,估计就到了1024了,真的这么准啊

    里面输出的内容如下:

    guide.yun 32566     u0_a81  515       ???                ???       ???        ??? anon_inode:[eventpoll]
    guide.yun 32566     u0_a81  517       ???                ???       ???        ??? socket:[14635912]
    guide.yun 32566     u0_a81  518       ???                ???       ???        ??? pipe:[14643795]
    guide.yun 32566     u0_a81  520       ???                ???       ???        ??? pipe:[14638969]
    guide.yun 32566     u0_a81  522       ???                ???       ???        ??? pipe:[14638969]
    guide.yun 32566     u0_a81  523       ???                ???       ???        ??? anon_inode:[eventpoll]
    guide.yun 32566     u0_a81  524       ???                ???       ???        ??? pipe:[14638726]
    guide.yun 32566     u0_a81  527       ???                ???       ???        ??? pipe:[14638726]
    guide.yun 32566     u0_a81  528       ???                ???       ???        ??? anon_inode:[eventpoll]
    guide.yun 32566     u0_a81  530       ???                ???       ???        ??? pipe:[14645682]
    guide.yun 32566     u0_a81  531       ???                ???       ???        ??? anon_inode:[eventpoll]
    guide.yun 32566     u0_a81  532       ???                ???       ???        ??? pipe:[14638731]
    guide.yun 32566     u0_a81  533       ???                ???       ???        ??? anon_inode:[eventpoll]
    guide.yun 32566     u0_a81  534       ???                ???       ???        ??? anon_inode:[eventpoll]
    guide.yun 32566     u0_a81  535       ???                ???       ???        ??? socket:[14635913]
    guide.yun 32566     u0_a81  536       ???                ???       ???        ??? pipe:[14643801]
    guide.yun 32566     u0_a81  537       ???                ???       ???        ??? anon_inode:[eventpoll]
    guide.yun 32566     u0_a81  539       ???                ???       ???        ??? anon_inode:[eventpoll]
    guide.yun 32566     u0_a81  542       ???                ???       ???        ??? pipe:[14643801]
    guide.yun 32566     u0_a81  543       ???                ???       ???        ??? anon_inode:[eventpoll]
    guide.yun 32566     u0_a81  545       ???                ???       ???        ??? socket:[14644273]
    guide.yun 32566     u0_a81  550       ???                ???       ???        ??? /storage/emulated/0/ads/gifDeepSleepBg.gif
    guide.yun 32566     u0_a81  mem       ???              00:04         0       2678 /dev/ashmem/dalvik-main
    

      但是这些句柄都是被谁占着呢?占用最多的好像就3种:pipe、socket、anon_inode。

    分别去查这三中类型的意义:pipe和socket跟linux中的通信相关,socket不用猜了,肯定是跟socket连接有关系,但是pipe呢?记得学java的输入输出六的时候有管道的概念,至于anon_inode就不知道是啥了,先从socket入手吧,看看socket有没有close,在仔细的阅读了socket这部分的逻辑代码后,很失望,每一次连接失败都close掉了,好了,放弃,继续查java的输入输出流

    通过搜索项目代码中全部的inputstream、outputstream、bufferReader、fileinputstream等,还真的发现有很多的流没有关闭,大概有三四百行的代码,经过四个小时的改造后重新发版,然后测试。

    昨天下午六点多开始发布,到晚上九点多走的时候还没有闪退,终于高高兴兴的回家了,看来这次的改动还是有效的。估计没啥问题了。

    然而想不到的是,今天早上八点四十到了公司发现,程序还是退出了,好吧,抓日志

    拿到log后分析,今天凌晨3.52分闪退了……还是没有坚持到最后啊。

    于是打开程序,继续分析句柄有没有增长,lsof命令,发现句柄数还在不断增长,只是速度比修改之前慢了很多。这次不能等到他崩溃再抓日志了,因为这次可能要等十个小时左右,不太现实。

    除了socket和输入输出流,还有什么会占用句柄呢?线程?只能猜测一下了,查一下线程

    adb shell
    ps -t |grep <pid>
    

      查一下我们程序的具体线程都有哪些,不查不要紧,一查就发现问题了,其中有个负责与底盘创建socket连接的线程的数量一直在不断的增长:

    u0_a74    21569 21513 918544 50684 c014570c b6e2640c S ConnectHelper
    u0_a74    21689 21513 918544 50684 c014570c b6e2640c S ConnectHelper
    u0_a74    21805 21513 918544 50684 c014570c b6e2640c S ConnectHelper
    u0_a74    21909 21513 918544 50684 c014570c b6e2640c S ConnectHelper
    u0_a74    22032 21513 918544 50684 c014570c b6e2640c S ConnectHelper
    u0_a74    22154 21513 918544 50684 c014570c b6e2640c S ConnectHelper
    u0_a74    22277 21513 918544 50684 c014570c b6e2640c S ConnectHelper
    u0_a74    22375 21513 918544 50684 c014570c b6e2640c S ConnectHelper
    u0_a74    22493 21513 918544 50684 c014570c b6e2640c S ConnectHelper
    u0_a74    22619 21513 918544 50684 c014570c b6e2640c S ConnectHelper
    u0_a74    22741 21513 918544 50684 c014570c b6e2640c S ConnectHelper
    u0_a74    22861 21513 918544 50684 c014570c b6e2640c S ConnectHelper
    u0_a74    22962 21513 918544 50684 c014570c b6e2640c S ConnectHelper
    u0_a74    23079 21513 918544 50684 c014570c b6e2640c S ConnectHelper
    u0_a74    23198 21513 918544 50684 c014570c b6e2640c S ConnectHelper
    u0_a74    23339 21513 918544 50684 c014570c b6e2640c S ConnectHelper
    u0_a74    23456 21513 918544 50684 c014570c b6e2640c S ConnectHelper
    u0_a74    23552 21513 918544 50684 c014570c b6e2640c S ConnectHelper
    u0_a74    23669 21513 918544 50684 c014570c b6e2640c S ConnectHelper
    u0_a74    23787 21513 918544 50684 c014570c b6e2640c S ConnectHelper
    u0_a74    23905 21513 918544 50684 c00827e0 b6dfb964 S Thread-1452
    u0_a74    23906 21513 918544 50684 c00827e0 b6dfb964 S Thread-1453
    u0_a74    23907 21513 918544 50684 c011b0bc b6e25bf4 S ConnectHelper
    

      肯定是这个没有释放造成的,由于这是一个线程,如果不用了就关掉就好了,于是在重连socket的时候,把这个线程中断掉就好,interrupt。改了后重新发布程序。看线程有没有在涨。

    然而,神奇的现象又发生了,线程居然还在涨,怎么可能,这个connecthelper没有被中断吗?为什么呢,仔细看代码才知道,这哪里是一个Thread,这是一个handlerThread,handlerThread虽然继承自Thread,但是它里面有looper,所以interrupt是不行的,这里需要调用quit(),修改后再试,果然线程数不涨了,于是兴高采烈的又发版了。

    由于我们的是sdk,发出去的有多个程序在用,我修改的程序中句柄数真的就控制住了,再也不涨了,然后另外一个程序居然再次出现了多个ConnectHelper,这又是什么鬼?

    于是再次分析我们的sdk代码,connectHelper作为一个线程,实质上是在一个叫做ConnectManager的管理类里去new的,ConnectManager每实例化一次,也就会多一个ConnectHelper。于是问题查找方向也就明确了,到底是谁又创建了多个ConnectManager。经过查找发现有两个地方会不断的创建新的ConnectManager,一个是我们与底盘通信的心跳service,如果连不上会每隔30s重新创建Connectmanager,并且旧的不销毁!另外一个控制机器人与底盘连接的RobotConnectAction也是一样,这两个地方都会造成ConnectManager对象的泄露从而会造成ConnectHelper泄露,找到这个问题后也就好解决了,就是在重新连接前把旧的销毁掉。

    盖好后重新测试,ok了,句柄数稳定了,经过四个小时的等待,一直也没有增长,终于可以休息了。

    总结下,遇到这种问题还是我们写代码不够规范造成的,输入输出流打开了一定要记得关闭,线程开启了如果不用了也要记得回收,还有一些对象如果需要重新初始化,那么旧的不用的对象也要回收,这样才能保证程序能够长久稳定的运行。我想:还好这个项目没有用到数据库,要不然可能还会遇到cursor的泄露,哈哈

  • 相关阅读:
    <linux程序设计> 第四章 [ 程序参数 / 环境变量 / 日期与时间]
    阻塞分析
    架构设计分类
    软件测试
    C#中常用的加密类
    SQL2005
    用异或的性质实现简单加密解密
    在sql中取系统时间?日期?年?
    SQL Server常用到的几个设置选项
    Connection Command[ExecuteNonQuery ExecuteScalar ExecuteReader] DataReader DataAdapter DataSet
  • 原文地址:https://www.cnblogs.com/dongweiq/p/9494033.html
Copyright © 2011-2022 走看看