200字范文,内容丰富有趣,生活中的好帮手!
200字范文 > Android FD 文件描述符 泄露总结

Android FD 文件描述符 泄露总结

时间:2022-08-14 17:43:43

相关推荐

Android FD 文件描述符 泄露总结

简述

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

-12-13 14:33:47.302 878 1017 F libc : FORTIFY: FD_SET: file descriptor >= FD_SETSIZE-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/fdlrwx------ u0_a55 u0_a55 -01-21 15:42 0 -> /dev/nulllrwx------ u0_a55 u0_a55 -01-21 15:42 1 -> /dev/nulll-wx------ u0_a55 u0_a55 -01-21 15:42 10 -> /dev/cpuctl/taskslrwx------ u0_a55 u0_a55 -01-21 15:42 11 -> anon_inode:[eventfd]l-wx------ u0_a55 u0_a55 -01-21 15:42 12 -> /dev/cpuctl/bg_non_interactive/taskslrwx------ u0_a55 u0_a55 -01-21 15:42 13 -> anon_inode:[eventpoll]lrwx------ u0_a55 u0_a55 -01-21 15:42 14 -> socket:[10778]lr-x------ u0_a55 u0_a55 -01-21 15:42 15 -> pipe:[10779]l-wx------ u0_a55 u0_a55 -01-21 15:42 16 -> pipe:[10779]lrwx------ u0_a55 u0_a55 -01-21 15:42 17 -> socket:[10783]lr-x------ u0_a55 u0_a55 -01-21 15:42 18 -> /data/app/com.example.kotlintest-1/base.apklrwx------ u0_a55 u0_a55 -01-21 15:20 19 -> anon_inode:[eventfd]lrwx------ u0_a55 u0_a55 -01-21 15:42 2 -> /dev/nulllrwx------ u0_a55 u0_a55 -01-21 15:42 20 -> socket:[9794]lrwx------ u0_a55 u0_a55 -01-21 15:42 21 -> anon_inode:[eventpoll]lrwx------ u0_a55 u0_a55 -01-21 15:20 22 -> /dev/goldfish_pipelrwx------ u0_a55 u0_a55 -01-21 15:42 23 -> socket:[10790]lrwx------ u0_a55 u0_a55 -01-21 15:42 24 -> /dev/goldfish_pipelrwx------ u0_a55 u0_a55 -01-21 15:42 25 -> /dev/goldfish_pipelrwx------ u0_a55 u0_a55 -01-21 15:42 26 -> socket:[10795]lrwx------ u0_a55 u0_a55 -01-21 15:42 27 -> /dev/goldfish_sync

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

FD的类型如下图所示

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

root@generic_x86:/ # ulimit -n1024

FD泄漏

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

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

“Too many open files”\“Could not allocate JNI Env”\“Could not allocate dup blob fd”\“Could not read input channel file descriptors from parcel”\"pthread_create * "\“InputChannel is not initialized”\“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 -01-24 11:26 30 -> /data/data/com.example.kotlintest/cache/testFdFilel-wx------ u0_a55 u0_a55 -01-24 11:26 31 -> /data/data/com.example.kotlintest/cache/testFdFilel-wx------ u0_a55 u0_a55 -01-24 11:26 32 -> /data/data/com.example.kotlintest/cache/testFdFilel-wx------ u0_a55 u0_a55 -01-24 11:26 33 -> /data/data/com.example.kotlintest/cache/testFdFilel-wx------ u0_a55 u0_a55 -01-24 11:26 34 -> /data/data/com.example.kotlintest/cache/testFdFilel-wx------ u0_a55 u0_a55 -01-24 11:26 35 -> /data/data/com.example.kotlintest/cache/testFdFilel-wx------ u0_a55 u0_a55 -01-24 11:26 38 -> /data/data/com.example.kotlintest/cache/testFdFile

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

out.close()

Looper、HandlerThread

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

//1.HandlerThreadval handlerThread = HandlerThread("test")handlerThread.start()//2.Thread+LooperThread {Looper.prepare()Looper.loop()}.start()

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

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

//1handlerThread.quitSafely()//2Looper.myLooper().quit()

Cursor

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

db = ordersDBHelper.getReadableDatabase();Cursor cursor = db.query(...);while (cursor.moveToNext()) {//......}if(flag){//某种原因导致retrnreturn;}//不调用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: mainProcess: com.example.kotlintest, PID: 4333java.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,搜索日志TAGStrictMode定位出问题的代码

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应用的文件描述符可以分为几类,通过对比哪一类文件描述符数量过高,来缩小问题范围。

dump系统信息

通过dumpsys window ,查看是否有异常window。用于解决 InputChannel 相关的泄漏问题。

如下,出现了很多个Window{6541819 u0 com.example.kotlintest/com.example.kotlintest.FDActivity},则可以从该Activity查找错误

D:\>adb shell dumpsys windowWindow #38 Window{6541819 u0 com.example.kotlintest/com.example.kotlintest.FDActivity}:mOwnerUid=10055 mShowToOwnerOnly=true package=com.example.kotlintest appop=NONEWindowStateAnimator{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=NONEWindowStateAnimator{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=NONEWindowStateAnimator{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=NONEWindowStateAnimator{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=NONEWindowStateAnimator{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=NONEWindowStateAnimator{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=NONEWindowStateAnimator{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=NONEWindowStateAnimator{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=NONEWindowStateAnimator{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=NONEWindowStateAnimator{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创建失败。

感谢文档:

一文帮你搞懂 Android文件描述符androidFD泄露问题总结

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。