简述
最近在项目中碰到一个跟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的类型如下图所示
Android系统中可以打开的文件描述符是有上限的,所以分到每一个进程可打开的文件描述符也是有限的。可以通过命令 ulimit -n 查看,Linux Android默认是1024,比较新款的Android设备大部分已经是大于1024的
root@generic_x86:/ # ulimit -n
1024
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 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应用的文件描述符可以分为几类,通过对比哪一类文件描述符数量过高,来缩小问题范围。
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创建失败。
感谢文档:
————————————————
版权声明:本文为CSDN博主「仙剑冲锋」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/hjc273928/article/details/127730618