欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

详解Android文件描述符

程序员文章站 2024-01-17 08:07:16
介绍文件描述符的概念以及工作原理,并通过源码了解 android 中常见的 fd 泄漏。一、什么是文件描述符?文件描述符是在 linux 文件系统的被使用,由于android基 于linux系统,所以...

介绍文件描述符的概念以及工作原理,并通过源码了解 android 中常见的 fd 泄漏。

一、什么是文件描述符?

文件描述符是在 linux 文件系统的被使用,由于android基 于linux 系统,所以android也继承了文件描述符系统。我们都知道,在 linux 中一切皆文件,所以系统在运行时有大量的文件操作,内核为了高效管理已被打开的文件会创建索引,用来指向被打开的文件,这个索引即是文件描述符,其表现形式为一个非负整数。

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

详解Android文件描述符

上图中 箭头前的数组部分是文件描述符,箭头指向的部分是对应的文件信息。

详解Android文件描述符

android系统中可以打开的文件描述符是有上限的,所以分到每一个进程可打开的文件描述符也是有限的。可以通过命令 cat /proc/sys/fs/file-max 查看所有进程允许打开的最大文件描述符数量。

详解Android文件描述符

当然也可以查看进程的允许打开的最大文件描述符数量。linux默认进程最大文件描述符数量是1024,但是较新款的android设置这个值被改为32768。

详解Android文件描述符

可以通过命令 ulimit -n 查看,linux 默认是1024,比较新款的android设备大部分已经是大于1024的,例如我用的测试机是:32768。

通过概念性的描述,我们知道系统在打开文件的时候会创建文件操作符,后续就通过文件操作符来操作文件。那么,文件描述符在代码上是怎么实现的呢,让我们来看一下linux中用来描述进程信息的 task_struct 源码。

task_struct 是 linux  内核中描述进程信息的对象,其中files指向一个文件指针数组 ,这个数组中保存了这个进程打开的所有文件指针。 每一个进程会用 files_struct 结构体来记录文件描述符的使用情况,这个 files_struct 结构体为用户打开表,它是进程的私有数据,其定义如下:

一般情况,“文件描述符”指的就是文件指针数组 files 的索引。

linux  在2.6.14版本开始通过引入struct fdtable作为file_struct的间接成员,file_struct中会包含一个struct fdtable的变量实例和一个struct fdtable的类型指针。

在file_struct初始化创建时,fdt指针指向的其实就是当前的的变量fdtab。当打开文件数超过初始设置的大小时,file_struct发生扩容,扩容后fdt指针会指向新分配的fdtable变量。

rcu(read-copy update)是数据同步的一种方式,在当前的linux内核中发挥着重要的作用。

rcu主要针对的数据对象是链表,目的是提高遍历读取数据的效率,为了达到目的使用rcu机制读取数据的时候不对链表进行耗时的加锁操作。这样在同一时间可以有多个线程同时读取该链表,并且允许一个线程对链表进行修改(修改的时候,需要加锁)。

rcu适用于需要频繁的读取数据,而相应修改数据并不多的情景,例如在文件系统中,经常需要查找定位目录,而对目录的修改相对来说并不多,这就是rcu发挥作用的最佳场景。

struct file 处于内核空间,是内核在打开文件时创建,其中保存了文件偏移量,文件的inode等与文件相关的信息,在 linux  内核中,file结构表示打开的文件描述符,而inode结构表示具体的文件。在文件的所有实例都关闭后,内核释放这个数据结构。

整体的数据结构示意图如下:

详解Android文件描述符

到这里,文件描述符的基本概念已介绍完毕。

二、文件描述符的工作原理

上文介绍了文件描述符的概念和部分源码,如果要进一步理解文件描述符的工作原理,需要查看由内核维护的三个数据结构。

详解Android文件描述符

i-node是 linux  文件系统中重要的概念,系统通过i-node节点读取磁盘数据。表面上,用户通过文件名打开文件。实际上,系统内部先通过文件名找到对应的inode号码,其次通过inode号码获取inode信息,最后根据inode信息,找到文件数据所在的block,读出数据。

三个表的关系如下:

详解Android文件描述符

进程的文件描述符表为进程私有,该表的值是从0开始,在进程创建时会把前三位填入默认值,分别指向 标准输入流,标准输出流,标准错误流,系统总是使用最小的可用值。

正常情况一个进程会从fd[0]读取数据,将输出写入fd[1],将错误写入fd[2]

每一个文件描述符都会对应一个打开文件,同时不同的文件描述符也可以对应同一个打开文件。这里的不同文件描述符既可以是同一个进程下,也可以是不同进程。

每一个打开文件也会对应一个i-node条目,同时不同的文件也可以对应同一个i-node条目。

光看对应关系的结论有点乱,需要梳理每种对应关系的场景,帮助我们加深理解。

详解Android文件描述符

问题:如果有两个不同的文件描述符且最终对应一个i-node,这种情况下对应一个打开文件和对应多个打开文件有什么区别呢?

答:如果对一个打开文件,则会共享同一个文件偏移量。

举个例子:

fd1和fd2对应同一个打开文件句柄,fd3指向另外一个文件句柄,他们最终都指向一个i-node。

如果fd1先写入“hello”,fd2再写入“world”,那么文件写入为“helloworld”。

fd2会在fd1偏移之后添加写,fd3对应的偏移量为0,所以直接从开始覆盖写。

三、android中fd泄漏场景

上文介绍了 linux 系统中文件描述符的含义以及工作原理,下面我们介绍在android系统中常见的文件描述符泄漏类型。

3.1 handlerthread泄漏

handlerthread是android提供的带消息队列的异步任务处理类,他实际是一个带有looper的thread。正常的使用方法如下:

handlerthread在不需要使用的时候,需要调用上述代码中的release方法来释放资源,比如在activity退出时。另外全局的handlerthread可能存在被多次赋值的情况,需要做空判断或者先释放再赋值,也需要重点关注。

handlerthread会泄漏文件描述符的原因是使用了looper,所以如果普通thread中使用了looper,也会有这个问题。下面让我们来分析一下looper的代码,查看到底是在哪里调用的文件操作。

handlerthread在run方法中调用looper.prepare();

looper在构造方法中创建messagequeue对象。

messagequeue,也就是我们在handler学习中经常提到的消息队列,在构造方法中调用了native层的初始化方法。

messagequeue对应native代码,这段代码主要是初始化了一个nativemessagequeue,然后返回一个long型到java层。

nativemessagequeue初始化方法中会先判断是否存在当前线程的native层的looper,如果没有的就创建一个新的looper并保存。

在looper的构造函数中,我们发现“eventfd”,这个很有文件描述符特征的方法。

从c++代码注释中可以知道eventfd函数会返回一个新的文件描述符。

3.2 io泄漏

io操作是android开发过程中常用的操作,如果没有正确关闭流操作,除了可能会导致内存泄漏,也会导致fd的泄漏。常见的问题代码如下:

如果在流操作过程中发生异常,就有可能导致泄漏。正确的写法应该是在final块中关闭流。

同样,我们在从源码中寻找流操作是如何创建文件描述符的。首先,查看 fileoutputstream 的构造方法 ,可以发现会初始化一个名为fd的 filedescriptor 变量,这个 filedescriptor 对象是java层对native文件描述符的封装,其中只包含一个int类型的成员变量,这个变量的值就是native层创建的文件描述符的值。

open方法会直接调用jni方法open0.

tips:  我们在看android源码时常常遇到native方法,通过android studio无法跳转查看,可以在 androidxref 网站,通过“java类名_native方法名”的方法进行搜索。例如,这可以搜索 fileoutputstream_open0 。

接下来,让我们进入native方法查看对应实现。

在fileopen方法中,通过handleopen生成native层的文件描述符(fd),这个fd就是这个所谓对面的文件描述符。

到这里就结束了吗?

回到开始,fileoutputstream构造方法中初始化了java层的文件描述符类 filedescriptor,目前这个对象中的文件描述符的值还是初始的-1,所以目前它还是一个无效的文件描述符,native层完成fd创建后,还需要把fd的值传到 java层。

我们再来看set_fd这个宏的定义,在这个宏定义中,通过反射的方式给java层对象的成员变量赋值。由于上文内容可知,open0是对象的jni方法,所以宏中的this,就是初始创建的fileoutputstream在java层的对象实例。

而fid则会在native代码中提前初始化好。

收,到这里fileoutputstream的初始化跟进就完成了,我们已经找到了底层fd初始化的路径。android的io操作还有其他的流操作类,大致流程基本类似,这里不再细述。

并不是不关闭就一定会导致文件描述符泄漏,在流对象的析构方法中会调用close方法,所以这个对象被回收时,理论上也是会释放文件描述符。但是最好还是通过代码控制释放逻辑。

3.3 sqlite泄漏

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

按照理解query操作应该会导致文件描述符泄漏,那我们就从query方法的实现开始分析。

然而,在query方法中并没有发现文件描述符相关的代码。

经过测试发现,movetonext 调用后才会导致文件描述符增长。通过query方法可以获取cursor的实现类sqlitecursor。

在sqlitecursor的父类找到movetonext的实现。getcount 是抽象方法,在子类sqlitecursor实现。

getcount 方法中对成员变量mcount做判断,如果还是初始值,则会调用fillwindow方法。

clearorcreatewindow 实现又回到父类 abstractwindowedcursor 中。

在cursorwindow的构造方法中,通过nativecreate方法调用到native层的初始化。

在c++代码中会继续调用一个native层cursorwindow的create方法。

在cursorwindow的create方法中,我们可以发现fd创建相关的代码。

ashmem_create_region 方法最终会调用到open函数打开文件并返回系统创建的文件描述符。这部分代码不在赘述,有兴趣的可以自行查看 。

native完成初始化会把fd信息保存在cursorwindow中并会返回一个指针地址到java层,java层可以通过这个指针操作c++层对象从而也能获取对应的文件描述符。

3.4 inputchannel 导致的泄漏

windowmanager.addview  

通过windowmanager反复添加view也会导致文件描述符增长,可以通过调用removeview释放之前创建的fd。

windowmanagerimpl中的addview最终会走到viewrootimpl的setview。

setview中会创建inputchannel,并通过binder机制传到服务端。

addtodisplay是一个aidl方法,它的实现类是源码中的session。最终调用的是 windowmanagerservice 的 addwindow 方法。

wms在 addwindow 方法中创建 inputchannel 用于通讯。

在 openinputchannel 中创建 inputchannel ,并把客户端的传回去。

inputchannel 的 openinputchannelpair 会调用native的 nativeopeninputchannelpair ,在native中创建两个带有文件描述符的 socket 。

windowmanager 的分析涉及wms,wms内容比较多,本文重点关注文件描述符相关的内容。简单的理解,就是进程间通讯会创建socket,所以也会创建文件描述符,而且会在服务端进程和客户端进程各创建一个。另外,如果系统进程文件描述符过多,理论上会造成系统崩溃。

四、如何排查

如果你的应用收到如下这些崩溃堆栈,恭喜你,你的应用存在文件描述符泄漏。

  • abort message 'could not create instance too many files'
  • could not read input file descriptors from parcel
  • socket failed:emfile (too many open files)
  • ...

文件描述符导致的崩溃往往无法通过堆栈直接分析。道理很简单: 出问题的代码在消耗文件描述符同时,正常的代码逻辑可能也同样在创建文件描述符,所以崩溃可能是被正常代码触发了。

4.1 打印当前fd信息

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

详解Android文件描述符

4.2 dump系统信息

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

4.3 线上监控

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

4.4 排查循环打印的日志

除了直接对 fd相关的信息进行分析,还需要关注logcat中是否有频繁打印的信息,例如:socket创建失败。

以上就是详解android 文件描述符的详细内容,更多关于android文件描述符的资料请关注其它相关文章!