Android-fd(文件描述符)

进程中android中io、socket、thread对于操作系统是什么?最多能创建多少个?

前言

去年工作中发现应用跑了很长一段时间后,突然:

  • 数据库打不开
  • sharedpreferences无法读取
  • 网络无法连接
  • 文件不可读写
    总之就是一切涉及IO的操作都不能做。其中找了很多办法来试图解决这个问题,都是不痛不痒的无法定位到问题所在。如追溯日志、debug立即调试均无异常。

最终在使用多机型和不同的android版本上复现了。复现的步骤比较粗暴,就是把网络断掉。然后让程序跑一会儿。也是后面才发现可以这样复现,之前每次要跑十几个小时才会出一次这样的问题。还是非必现的。

一般的应用也许很难碰到这样的场景,我先讲一个前置条件。

  • 频繁创建socket
  • 频繁的IO读写
  • 频繁的线程创建和关闭

先简单说一下这个问题的产生原因和解决办法,后面我们再从中分析原理。

应用具备了和服务端保持长链接的能力,保持实时push数据给服务器,由于我们做了重连机制,最后发现在弱网环境下由于消息无法送达socket会重复的断开和重连服务器。又因为使用socket是在java层写的close()并非真实关闭释放,android虚拟机在4.4以后的版本在某些场景下就有可能无法成功释放linux层fd。导致每重连一次fd就会+1。最终会因为fd达到了linux默认值上限后而无法创建导致android层看不到异常也无法正常使用IO相关的api工作。

解决的办法就是优化重连机制,把socket下沉到native去维护。java不要在干这件事了。

fd是什么

fd(file descriptor)

直译就是文件描述符,它表示当前打开的文件索引。不管我们是创建socket、io、线程最终对应到linux底层都是文件描述符的形式,相信对linux有认识的同学都听说过一句话叫:“Linux下,一切皆文件”。在这个平台上其实不管我们做的什么操作,最终都对应成了文件。我们看一下维基百科怎么说的吧。

维基百科

文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。

定位

如果你现在手上有一台root过的android设备,可以通过shell到/proc/{PID}/fd目录下使用查看当前应用的文件描述符数量。如下:

1
2
3
su
cd /proc/{PID}/fd
ls | wc -w

其中PID是进程id。去年验证该问题的时候我采用了观察断网前后来排查该错误,以下是当时的笔记部分:

  • 断网前:95左右跳动(这里的跳动是正常close)
  • 断网后:从95自增飙升到一千多随后开始IO不可用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ls -l
lr-x------ 1 u0_a52 u0_a52 64 2017-06-07 10:33 72 -> /system/app/webview/webview.apk
lr-x------ 1 u0_a52 u0_a52 64 2017-06-07 10:33 73 -> /dev/urandom
lrwx------ 1 u0_a52 u0_a52 64 2017-06-07 10:33 74 -> anon_inode:[eventpoll]
lrwx------ 1 u0_a52 u0_a52 64 2017-06-07 10:33 75 -> socket:[13214]
lrwx------ 1 u0_a52 u0_a52 64 2017-06-07 10:33 76 -> socket:[13215]
lr-x------ 1 u0_a52 u0_a52 64 2017-06-07 10:33 77 -> pipe:[13216]
l-wx------ 1 u0_a52 u0_a52 64 2017-06-07 10:33 78 -> pipe:[13216]
lrwx------ 1 u0_a52 u0_a52 64 2017-06-07 10:33 79 -> anon_inode:[eventpoll]
lrwx------ 1 u0_a52 u0_a52 64 2017-06-07 10:33 8 -> /dev/binder
lrwx------ 1 u0_a52 u0_a52 64 2017-06-07 10:33 80 -> socket:[13218]
lrwx------ 1 u0_a52 u0_a52 64 2017-06-07 10:33 81 -> socket:[13219]
lrwx------ 1 u0_a52 u0_a52 64 2017-06-07 10:33 82 -> /dev/ashmem
lr-x------ 1 u0_a52 u0_a52 64 2017-06-07 10:33 83 -> pipe:[13220]
l-wx------ 1 u0_a52 u0_a52 64 2017-06-07 10:33 84 -> pipe:[13220]
lrwx------ 1 u0_a52 u0_a52 64 2017-06-07 10:33 85 -> socket:[17045]

可以看到有pipe和socket,这里的pipe是Linux进程通信管道。暂时不去管它。socket的数目明显不对。断网重连网络,再到释放网络。这里的释放根本不起作用。虽然java调用了close方法。但是在fd里的描述符仍然被持续占用。到占满后就导致不允许再创建任何文件描述符。

带着这样的疑问,我又尝试在4.4和6.0的android版本中做测试,发现在android4.4中会释放。到android 6.0却不会释放。由于真设备是从4.4升级到了6.0在当初开发这个模块的时候问题并没有暴露出来。而换了6.0以后也需要再非常极端的网络不稳定和数个小时候才能复现。问题迟迟被拖了这么久没有被解决。

从用户反馈的角度来看就是设备(应用)死机了。表象上去看没有任何错误日志,实际是日志系统并没有记录下来,因为压根没法记录(IO不可用)。

  • 1.本地log自动记录失效。
  • 2.网络上报不可用。

fd描述符被占满了导致文件和网络都不可用,这就是为什么,线上的设备(应用)挂了,而去拿日志的时候什么都没有的原因。

fd最大值

我们可以通过/proc/pid/limits,来查看当前进程允许使用的IO数目总量。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
hx_s905x:/proc/4973 # cat limits                                                                      
Limit Soft Limit Hard Limit Units
Max cpu time unlimited unlimited seconds
Max file size unlimited unlimited bytes
Max data size unlimited unlimited bytes
Max stack size 8388608 unlimited bytes
Max core file size 0 unlimited bytes
Max resident set unlimited unlimited bytes
Max processes 6811 6811 processes
Max open files 1024 1024 files
Max locked memory 65536 65536 bytes
Max address space unlimited unlimited bytes
Max file locks unlimited unlimited locks
Max pending signals 6811 6811 signals
Max msgqueue size 819200 819200 bytes
Max nice priority 40 40
Max realtime priority 0 0
Max realtime timeout unlimited unlimited us

这里的 Max open files 字段就是指明我们的fd描述符允许被创建的总数。

排查总结

  • fd允许的最大值: /proc/pid/limits
  • 获取当前应用的pid: ps
  • 当前应用的状态: /proc/pid/fd
  • 数量: ls | wc -w

我们只需要上面这些命令就可以了。检测的方式就是查看数目的抖动情况,如果维持比较平衡。不会出现不释放的情况则为正常。如果抖动情况很大,或者只增长不减少则存在fd泄漏。目前为止认知受限,还没弄明白为什么java层已经明确调用了释放但底层却没有释放的原因。不过我们可以在应用层控制频繁的创建和及时的检测,一般的App应用很少会遇见这种问题,因为用户用完就关了App。但是做平板、机顶盒、大屏触控(突然想起TNT工作站)等需要长时间运行的app。涉及到了频繁的网络创建和IO操作的就有可能会碰到。

上面的内容大部分是去年做的笔记,其本质是如果我们无法了解外围环境本身,就很有可能在当前的认知维度上做出错误的判断。假如当时我咬定是rom的错误,而不去学linux,基本上这个问题也就没法解决了。经常我会犯认知错误,就是站在自己所了解的情况,去认为一件事。结果往往还是错误的。

随缘打赏!