Android FD 文件描述符 泄露总结

·  阅读 2320
Android FD 文件描述符 泄露总结

简述

最近在项目中碰到一个跟FD相关的crash,从log中获取到信息如下

2021-12-13 14:33:47.302   878  1017 F libc    : FORTIFY: FD_SET: file descriptor >= FD_SETSIZE
2021-12-13 14:33:47.302   878  1017 F libc    : Fatal signal 6 (SIGABRT), code -6 in tid 1017 (pool-2-thread-1)

经过一番奋斗终于解决,然后调研了下这个之前没碰到过的东西,发现还挺重要挺常见的,但是又不容易被发现,在此记录。

什么是FD

FD(File Descriptor)文件描述符在形式上是非负整数,它是一个索引值,指向内核为每个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在Linux系统中,一切设备都视作文件,文件描述符为Linux平台设备相关的编程提供了一个统一的方法。

FD作为文件句柄的实例,可以用来表示一个打开的文件,一个打开的网络流(socket),管道或者资源(如内存块),输入输出(in/out/error)。

可以通过命令 ls -l /proc/$pid/fd 查看当前进程文件描述符使用信息。

root@generic_x86:/ # ls -l /proc/2479/fd
lrwx------ u0_a55   u0_a55            2022-01-21 15:42 0 -> /dev/null
lrwx------ u0_a55   u0_a55            2022-01-21 15:42 1 -> /dev/null
l-wx------ u0_a55   u0_a55            2022-01-21 15:42 10 -> /dev/cpuctl/tasks
lrwx------ u0_a55   u0_a55            2022-01-21 15:42 11 -> anon_inode:[eventfd]
l-wx------ u0_a55   u0_a55            2022-01-21 15:42 12 -> /dev/cpuctl/bg_non_interactive/tasks
lrwx------ u0_a55   u0_a55            2022-01-21 15:42 13 -> anon_inode:[eventpoll]
lrwx------ u0_a55   u0_a55            2022-01-21 15:42 14 -> socket:[10778]
lr-x------ u0_a55   u0_a55            2022-01-21 15:42 15 -> pipe:[10779]
l-wx------ u0_a55   u0_a55            2022-01-21 15:42 16 -> pipe:[10779]
lrwx------ u0_a55   u0_a55            2022-01-21 15:42 17 -> socket:[10783]
lr-x------ u0_a55   u0_a55            2022-01-21 15:42 18 -> /data/app/com.example.kotlintest-1/base.apk
lrwx------ u0_a55   u0_a55            2022-01-21 15:20 19 -> anon_inode:[eventfd]
lrwx------ u0_a55   u0_a55            2022-01-21 15:42 2 -> /dev/null
lrwx------ u0_a55   u0_a55            2022-01-21 15:42 20 -> socket:[9794]
lrwx------ u0_a55   u0_a55            2022-01-21 15:42 21 -> anon_inode:[eventpoll]
lrwx------ u0_a55   u0_a55            2022-01-21 15:20 22 -> /dev/goldfish_pipe
lrwx------ u0_a55   u0_a55            2022-01-21 15:42 23 -> socket:[10790]
lrwx------ u0_a55   u0_a55            2022-01-21 15:42 24 -> /dev/goldfish_pipe
lrwx------ u0_a55   u0_a55            2022-01-21 15:42 25 -> /dev/goldfish_pipe
lrwx------ u0_a55   u0_a55            2022-01-21 15:42 26 -> socket:[10795]
lrwx------ u0_a55   u0_a55            2022-01-21 15:42 27 -> /dev/goldfish_sync

2022-01-21 15:42 20 -> socket:[9794],这里20就是文件描述符FD,socket:[9794] 就是指向的文件信息。

FD的类型如下图所示

FD类型说明
socket与网络请求相关
anon_inode:[eventpoll]HandlerThread 线程 Looper相关
anon_inode:[eventfd]HandlerThread 线程 Looper相关
anon_inode:[timerfd]系统文件描述符类型,和应用关系不大
anon_inode:[dmabuf]InputChannel泄露时增加明显
/vendor/一般是系统操作使用
/dev/ashmem数据库操作相关
pipe:一般是系统操作使用
/sys/一般是系统操作使用
/data/data/打开文件相关
/data/app/打开文件相关
/storage/emulate/0/打开文件相关

Android系统中可以打开的文件描述符是有上限的,所以分到每一个进程可打开的文件描述符也是有限的。可以通过命令 ulimit -n 查看,Linux Android默认是1024,比较新款的Android设备大部分已经是大于1024的

root@generic_x86:/ # ulimit -n
1024

FD泄漏

相比较传统的内存泄漏,FD泄漏在大部分情况下不会出现内存不足的情况,所以出现问题的时候会更加隐晦。由于发生FD泄漏的时候内存可能不会出现不足,所以不会出发系统的GC操作,导致只有通过crash进程的方式去自我恢复。事实上在很多情况下,就算触发系统GC,也不一定能够回收已经创建的句柄文件。

如下Java层的Error Msg均有fd泄漏的嫌疑:

  1. "Too many open files"\
  2. "Could not allocate JNI Env"\
  3. "Could not allocate dup blob fd"\
  4. "Could not read input channel file descriptors from parcel"\
  5. "pthread_create * "\
  6. "InputChannel is not initialized"\
  7. "Could not open input channel pair"

FD泄漏的场景

输入输出

输入输出流的使用在任何程序中都会比较频繁,像FileInputStream,FileOutputStream,FileReader,FileWriter 等输入输出如果不断创建但是不及时关闭,不仅可能造成内存的泄露了也可能会造成FD的溢出。每次new一个FileInputStream、FileOutputStream 都会在进程中创建一个FD, 用来指向这个打开的文件,而如果反复执行下面的代码,FD文件会持续不断地增加,直至超过1024出现FC。

val file = File(cacheDir, "testFdFile")
file.createNewFile()
val out = FileOutputStream(file)

在/proc/${进程id}/fd/ 目录下执行ls –l查看到增加的FD指向创建的文件,这里创建了不同的file,即使是对同一个文件,也会创建多个FD来指向这个打开的文件流。

l-wx------ u0_a55   u0_a55   2022-01-24 11:26 30 -> /data/data/com.example.kotlintest/cache/testFdFile
l-wx------ u0_a55   u0_a55   2022-01-24 11:26 31 -> /data/data/com.example.kotlintest/cache/testFdFile
l-wx------ u0_a55   u0_a55   2022-01-24 11:26 32 -> /data/data/com.example.kotlintest/cache/testFdFile
l-wx------ u0_a55   u0_a55   2022-01-24 11:26 33 -> /data/data/com.example.kotlintest/cache/testFdFile
l-wx------ u0_a55   u0_a55   2022-01-24 11:26 34 -> /data/data/com.example.kotlintest/cache/testFdFile
l-wx------ u0_a55   u0_a55   2022-01-24 11:26 35 -> /data/data/com.example.kotlintest/cache/testFdFile
l-wx------ u0_a55   u0_a55   2022-01-24 11:26 38 -> /data/data/com.example.kotlintest/cache/testFdFile

正确的做法是能够在final中将流进行关闭,这样无论中途是否出现异常导致程序中断,都会将流顺利关闭。

out.close()

Looper、HandlerThread

在Android中使用线程,尤其是HandlerThread要尤其的谨慎,必须要确保创建HandlerThread的函数不会被反复的调用导致线程反复的被创建。

//1.HandlerThread
val handlerThread = HandlerThread("test")
handlerThread.start()

//2.Thread+Looper
Thread {
    Looper.prepare()
    Looper.loop()
}.start()

而Looper对象初始化时Looper.prepare() 需要fd资源,而且是一个HandlerThread起来会消耗一对fd(eventFd和epollFd),这两个fd的目的也很明确,就是用来实现线程间通信的。

在不需要线程Loop的时候调用HandlerThead.quitSafely()或者HandlerThead.quit()销毁loop,释放句柄资源,如下:

//1
handlerThread.quitSafely()

//2
Looper.myLooper().quit()

Cursor

在日常开发中如果使用数据库SQLite管理本地数据,在数据库查询的cursor使用完成后,亦需要调用close方法释放资源,否则也有可能导致内存和文件描述符的泄漏。

db = ordersDBHelper.getReadableDatabase();
Cursor cursor = db.query(...);
while (cursor.moveToNext()) {
  //......
}
if(flag){
   //某种原因导致retrn
   return;
}
//不调用close,fd就会泄漏
cursor.close();

InputChannel

WindowManager.addView,通过WindowManager反复添加view也会导致文件描述符增长,可以通过调用removeView释放之前创建的FD。
而当我们show一个AlertDialog时,也会产生一个window,同样也会创建FD,当我们不停创建时候也会产生FD泄漏,如下:

for (index in 1 until 1024) {
    AlertDialog.Builder(this).show()
}
E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.kotlintest, PID: 4333
    java.lang.RuntimeException: Could not read input channel file descriptors from parcel.
        at android.view.InputChannel.nativeReadFromParcel(Native Method)
        at android.view.InputChannel.readFromParcel(InputChannel.java:148)
        at android.view.IWindowSession$Stub$Proxy.addToDisplay(IWindowSession.java:759)
        at android.view.ViewRootImpl.setView(ViewRootImpl.java:531)
        at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:310)
        at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:85)
        at android.app.Dialog.show(Dialog.java:319)
        at android.support.v7.app.AlertDialog$Builder.show(AlertDialog.java:1007)
        at com.example.kotlintest.FDActivity$onCreate$4.onClick(FDActivity.kt:36)
        at android.view.View.performClick(View.java:5198)
        at android.view.View$PerformClick.run(View.java:21147)
        at android.os.Handler.handleCallback(Handler.java:739)
        at android.os.Handler.dispatchMessage(Handler.java:95)
        at android.os.Looper.loop(Looper.java:148)
        at android.app.ActivityThread.main(ActivityThread.java:5417)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)

看到不仅demo app crash了,而且system_server也出现了异常crash,手机重启了,可怕!!!
足见fd泄漏问题的严重性,也了解到app异常也会影响到system_server的稳定性。

这里inputchannel也是需要fd资源。应用的input event由WindowManagerService管理,WMS内部会创建一个InputManager,两者通过InputChannel来完成,WMS需要注册两个InputChannel与InputManager连接,其中Server端InputChannel注册在InputManager(SystemServer),Client端注册在应用程序主线程中。InputChannel使用Ashmem匿名共享内存来传递数据,它由一个fd文件描述符指向,同时read端和write端各占用一个fd。创建一个新的Task时, server(system_server)和client(app)都会构建FD。addWindow的时候需要初始化Inputchannel去和InputManagerService进行跨进程通信来监控Input事件,本质上是初始化了一对socket文件进行通信

简单的理解,就是进程间通讯会创建socket,所以也会创建文件描述符,而且会在服务端进程和客户端进程各创建一个。另外,如果系统进程文件描述符过多,理论上会造成系统崩溃。

如何解决FD泄漏问题

StrictMode

使用StrictMode框架定位具体代码占用fd,搜索日志TAG StrictMode 定位出问题的代码

StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
    .detectDiskReads()
    .detectDiskWrites()
    .detectNetwork() // or .detectAll() for all detectable problems
    .penaltyLog()
    .build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
    .detectLeakedSqlLiteObjects()
    .detectLeakedClosableObjects()
    .penaltyLog()
    .penaltyDeath()
    .build());

不过,严格模式也并不能发现全部问题。我经历过使用了严格模式排查了之后,问题依然存在的状况。所以还需要一些其他手段。

打印当前FD信息

遇到FD泄漏问题如果能够复现,可以先尝试复现,然后通过命令 ‘ls -la /proc/$pid/fd’ 查看当前进程文件描述符的消耗情况。一般android应用的文件描述符可以分为几类,通过对比哪一类文件描述符数量过高,来缩小问题范围。

FD类型说明
socket检查网络请求
anon_inode检查HandlerThread 线程 Looper InputChannel
/dev/ashmem检查数据库操作
/data/data/ /data/app/ /storage/emulate/0/检查对应的文件是否打开未关闭

dump系统信息

通过dumpsys window ,查看是否有异常window。用于解决 InputChannel 相关的泄漏问题。 如下,出现了很多个Window{6541819 u0 com.example.kotlintest/com.example.kotlintest.FDActivity},则可以从该Activity查找错误

D:\>adb shell dumpsys window

Window #38 Window{6541819 u0 com.example.kotlintest/com.example.kotlintest.FDActivity}:
  mOwnerUid=10055 mShowToOwnerOnly=true package=com.example.kotlintest appop=NONE
  WindowStateAnimator{3c2a817 com.example.kotlintest/com.example.kotlintest.FDActivity}:
Window #37 Window{20c9363 u0 com.example.kotlintest/com.example.kotlintest.FDActivity}:
  mOwnerUid=10055 mShowToOwnerOnly=true package=com.example.kotlintest appop=NONE
  WindowStateAnimator{3301496 com.example.kotlintest/com.example.kotlintest.FDActivity}:
Window #36 Window{c67271d u0 com.example.kotlintest/com.example.kotlintest.FDActivity}:
  mOwnerUid=10055 mShowToOwnerOnly=true package=com.example.kotlintest appop=NONE
  WindowStateAnimator{5bb77b1 com.example.kotlintest/com.example.kotlintest.FDActivity}:
Window #35 Window{5a1a1c7 u0 com.example.kotlintest/com.example.kotlintest.FDActivity}:
  mOwnerUid=10055 mShowToOwnerOnly=true package=com.example.kotlintest appop=NONE
  WindowStateAnimator{3365a58 com.example.kotlintest/com.example.kotlintest.FDActivity}:
Window #34 Window{9508de1 u0 com.example.kotlintest/com.example.kotlintest.FDActivity}:
  mOwnerUid=10055 mShowToOwnerOnly=true package=com.example.kotlintest appop=NONE
  WindowStateAnimator{f70133b com.example.kotlintest/com.example.kotlintest.FDActivity}:
Window #33 Window{af9d1eb u0 com.example.kotlintest/com.example.kotlintest.FDActivity}:
  mOwnerUid=10055 mShowToOwnerOnly=true package=com.example.kotlintest appop=NONE
  WindowStateAnimator{4f965ca com.example.kotlintest/com.example.kotlintest.FDActivity}:
Window #32 Window{4465065 u0 com.example.kotlintest/com.example.kotlintest.FDActivity}:
  mOwnerUid=10055 mShowToOwnerOnly=true package=com.example.kotlintest appop=NONE
  WindowStateAnimator{e473d35 com.example.kotlintest/com.example.kotlintest.FDActivity}:
Window #31 Window{f3287cf u0 com.example.kotlintest/com.example.kotlintest.FDActivity}:
  mOwnerUid=10055 mShowToOwnerOnly=true package=com.example.kotlintest appop=NONE
  WindowStateAnimator{50f736c com.example.kotlintest/com.example.kotlintest.FDActivity}:
Window #30 Window{66632a9 u0 com.example.kotlintest/com.example.kotlintest.FDActivity}:
  mOwnerUid=10055 mShowToOwnerOnly=true package=com.example.kotlintest appop=NONE
  WindowStateAnimator{b90541f com.example.kotlintest/com.example.kotlintest.FDActivity}:
Window #29 Window{f9ae773 u0 com.example.kotlintest/com.example.kotlintest.FDActivity}:
  mOwnerUid=10055 mShowToOwnerOnly=true package=com.example.kotlintest appop=NONE
  WindowStateAnimator{6084bbe com.example.kotlintest/com.example.kotlintest.FDActivity}:
Window #28 Window{7b9b8ad u0 com.example.kotlintest/com.example.kotlintest.FDActivity}:

线上监控

如果是本地无法复现问题,可以尝试添加线上监控代码,定时轮询当前进程使用的FD数量,在达到阈值时,读取当前FD的信息,并传到后台分析,获取FD对应文件信息的代码如下。

val fdFile = File("/proc/" + android.os.Process.myPid() + "/fd/")
val files = fdFile.listFiles() // 列出当前目录下所有的文件
val length = files?.size; // 进程中的fd数量
Log.d(TAG, "listFd = " + android.os.Process.myPid() + " = " + length)
//列车FD以及其指向文件信息
files?.forEach { file ->
    try {
        val linkTarget = Os.readlink(file.absolutePath);
        Log.d(TAG, "$file====>$linkTarget")
    } catch (e: Exception) {
        Log.d(TAG, "$file====> error")
    }
}

排查循环打印的日志

关注logcat中是否有频繁打印的信息,例如:socket创建失败。

感谢文档:

  1. 一文帮你搞懂 Android文件描述符
  2. androidFD泄露问题总结
分类:
Android
标签:
分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改