前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入理解Android系统资源异常之文件描述符异常篇

深入理解Android系统资源异常之文件描述符异常篇

作者头像
233333
发布2022-07-12 17:06:37
2K0
发布2022-07-12 17:06:37
举报

一、引言

本文的目标是帮助大家深入理解Android系统资源异常之文件描述符异常,对于文件描述符异常的通用检测机制,当前包括fdtrack和fdsan两种机制展开剖析。

通过阅读本篇文章,期望读者可以了解到:

1)什么是文件描述符

2)linux kernel中如何使用文件描述符,来管理进程打开文件资源

3)android fdsan机制设计思路与实现

4)android fdtrack机制设计思路与实现

二、背景知识

1. 什么是文件描述符

文件描述符,即file descriptor,缩写为fd。

对于linux内核,所有打开的文件都是通过文件描述符引用,文件描述符实现为一个非负整数。

linux的设计哲学是一切兼文件。这句话的意思是,所有的系统资源都可以通过文件IO的方式进行访问。这就凸显了作为索引的文件描述符的重要性。

2. 获取fd的时机

当打开一个现有的文件,或创建一个新文件时,内核会向进程返回一个文件描述符。

当读、写一个文件时,使用open/create返回的文件描述符来标识该文件,将其作为参数传递给read或write。

3. fd取值范围的限制

文件描述符取值范围在[0~OPEN_MAX - 1],在早期的操作系统实现中OPEN_MAX取值很小,但对于现代的操作系统实现,文件描述符的变化范围几乎是无限制的,只受到系统的硬件配置、整型的字长以及系统管理员配置的软、硬限制的约束。

POSIX语义中,0、1、2这三个文件描述符被标准赋予特殊含义,分别指代标准输入STDIN_FILENO、标准输出STDOUT_FILENO、标准错误STDERR_FILENO。

系统中对每个进程可以打开最大fd数量的限制,可以通过命令ulimit -n查询。

也可以加上-S标识软限制,-H标识硬限制。

我这里分别查询了手机系统和服务器系统上的数值,手机系统上为 32768;服务器系统上为 20480000。

那么,手机上的这个fd最大限制数值是怎么来的呢?

标准linux实现中,在头文件include/uapi/linux/fs.h中有宏定义,标识了系统默认的软、硬限制。

代码语言:javascript
复制
#define INR_OPEN_CUR 1024 /* Initial setting for nfile rlimits */

#define INR_OPEN_MAX 4096 /* Hard limit for nfile rlimits */

android系统在core/rootdir/init.rc文件中,on early-init时候进行了重新设定。

# Allow up to 32K FDs per process

setrlimit nofile 32768 32768

4. 内核如何使用fd管理进程打开的文件

我们知道进程是操作系统资源管理的基本单元。

linux内核中使用struct task_struct来描述进程。

(1) struct task_struct

task_struct结构体中有一个字段files,对应的struct files_struct结构体用于管理进程打开的文件资源。

下面列出了task_struct中与文件资源管理相关的核心字段。

代码语言:javascript
复制
struct task_struct {

/* Open file information: */

struct files_struct *files;

};

可以看到files字段是一个指针,指向了一个struct files_struct的结构体。

(2) struct files_struct

files_struct结构体用于管理进程打开的所有文件资源。采用数组的方式来管理进程打开的文件,fd(非负整数)就作为数组的索引。

image
image

可以看到数组分布在两个地方:

1)struct file __rcu * fd_array[NR_OPEN_DEFAULT],是直接包含在files_struct结构体的静态数组部分,在64位系统上,NR_OPEN_DEFAULT对应为64;

2)struct fdtab,是一个动态数组结构,数组没有直接包含在files_struct结构体中,是根据需要动态分配的。

这种静态分配加动态扩展的方式,是软件设计中的常用技巧,是对性能与资源的tradeoff。

对于大部分只打开少量文件的进程来说,静态数组就可以满足需求。对于少部分需要打开更多文件的进程,当打开文件数量超过了静态数组的阈值后,会动态分配fdtab数组来进行扩展。

(3) struct fdtab

fdtab结构体是封装动态扩展fd数组的,其中关键字段为fd和max_fds。

image
image

fd字段是一个二级指针,指向一个数组(动态分配),数组元素类型为struct file *,与上面的静态数组fd_array一样。max_fds字段表明这个动态数组的大小。

再次重复强调一下,文件描述符fd,就是一个数组索引,用于索引进程(files_struct结构体中)打开的文件。

(4) struct file

fd本质是进程中的一个数组索引,索引的数组元素类型为struct file *,而struct file才是表示文件信息的正主。

下面列出了struct file结构体中,需要重点关注的几个字段。

代码语言:javascript
复制
struct file {

struct path f_path;

struct inode *f_inode; /* cached value */

const struct file_operations *f_op;

loff_t f_pos;

} __randomize_layout

__attribute__((aligned(4))); /* lest something weird decides that 2 is OK */

f_path用来表示文件名;

f_inode用来关联文件系统信息,这里的inode是vfs的inode类型,是基于具体的文件系统之上一层通用抽象;

f_pos表示当前文件的偏移,在进行实际IO的时候非常重要。f_pos会在open的时候设置成默认值,seek的时候修改为指定值。

(5) 文件描述符与文件关系

需要注意的是,struct files_struct结构体归属于某个进程,所以fd是进程内部的资源,用于管理本进程内打开的文件。

struct file结构体是系统级别,不归属于单个进程。多个进程可以打开同一个文件,使用自身的fd资源索引该文件。

下面引用自参考资料[2]中的一张图片来直观说明这种关系:

image
image

三、fdsan机制介绍

1. fdsan简介

(1) fdsan是什么

file descriptor sanitizer的缩写,是Android在Q版本中引入的一种文件描述符异常检测机制。

(2) fdsan可以干什么

可以检测fd ownership mis-handling这种类型的错误。

(3) fdsan怎么用

Android在Q版本引入的针对fd ownership mid-handling的异常检测机制。代码固化在bionic的libc库。在通过linker加载libc库时,fdsan相关初始化代码会自动导入。

所有包含了libc库的共享库以及可执行程序,已经包含了fdsan的基础设施,只要在代码中使用fdsan提供的API来检查文件打开与关闭操作即可:

1)android_fdsan_exchange_owner_tag,在打开文件后,紧接着调用该API设置owner tag

2)android_fdsan_close_with_tag,在关闭文件前,调用该API进行fdsanitizer检测

需要注意的是,fdsan的基础设施固化在libc库中,所以没有包含libc库的共享库或者可执行程序,无法使用该检测机制提供的能力。

当前AOSP代码中共享库与可执行程序已经包含了对fdsan的使用。

使用范例可以参考AOSP代码中的libziparchive实现。

在构造函数中,增加android_fdsan_exchange_owner_tag的调用。

image
image

在析构函数中增加android_fdsan_close_with_tag的调用。

image
image

其中通过GetOwnerTag,我们可以看到对于owner tag的构造。

image
image

这样所有使用的libziparchive的代码就包含了对于ANDROID_FDSAN_OWNER_TYPE_ZIPARCHIVE 类型fd的sanitizer检测。

2. 涉及代码路径

代码语言:javascript
复制
bionic/docs/fdsan.md

bionic/libc/include/android/fdsan.h

bionic/libc/private/bionic_fdsan.h

bionic/libc/bionic/fdsan.cpp

bionic/tests/fdsan_test.cpp

其中fdsan.md,是fdsan的manual文件;

fdsan.h,是fdsan方案的公共头文件,包含了API接口原型声明以及tag类型的枚举定义;

bionic_fdsan.h,是fdsan方案内部私有头文件,定义了进程内部存储fd关联tag信息的数据结构,按照静态数组(128)加动态扩展数组方式来实现存储结构;

fdsan.cpp,fdsan方案的实现文件;

fdsan_test.cpp,fdsan方案测试代码,针对各种类型的fd ownership mis-handling的模拟故障测试代码。

3. 设计思路解读

fdsan的设计思路浅显易懂:

1)打开已有文件或创建一个新文件的时候,在得到返回fd后,设置一个关联的tag,来标记fd的属主信息;

关闭文件前,检测fd关联的tag,判断是否符合预期(属主信息一致),符合就继续走正常文件关闭流程;如果不符合就是检测到异常,根据设置,调用对应的异常处理。

4. 实现代码解读

image
image

通过代码注释,可以看到如果没有主动设置tag,则文件打开相关操作,默认tag为0,关闭的时候也按照对应的默认tag(0)来做匹配检测。

(1) 关联tag格式定义

fdsan内部实现,使用struct FdEntry来表示与fd关联的tag,可以看到实际上就是一个u64原子类型的整数。

struct FdEntry {

_Atomic(uint64_t) close_tag = 0;

};

还有一个关键的枚举android_fdsan_owner_type给出了tag格式的解读。

定义在bionic/libc/include/android/fdsan.h头文件中。

image
image

可以看到将64bit的tag数据拆分为两个部分:最高字节用于标识type类型,剩下字节用于标识实际的owner tag。

从注释可以看到,android当前预定义了12种type,这12种之外的其他java对象,以及native指针type域都会对应到255。

对于通用java对象,type域定义为255,并使用对象的hashcode作为tag域的值;

对于native指针,整个close_tag取值为48bit虚拟地址的符号扩展,type域的值正好也是255,并且可以使用bit49~56的值来区分是native指针还是通用java对象类型。

这个可以从内部函数android_fdsan_get_tag_type的实现中,得到很好的解读。

image
image

(2) 实现内部存储tag数据结构定义

定义在bionic/libc/private/bionic_fdsan.h头文件中。

tag存储实体struct FdEntry定义:

image
image

动态扩展数组结构struct FdTableOverflow定义:

image
image

FdTable数据结构模板FdTableImpl定义:

image
image

包含如下重要成员:

error_level,用于控制检测到异常后的处理行为,放到该结构体里,可以实现每进程细粒度控制;

entries数组,静态数组,大小由模板特化的时候参数传入,对于大多数进程来说,需要打开文件数量有限,静态数组就可以满足存储需求;

overflow,动态数组,当进程打开文件数量超出静态数组阈值时候,动态分配;

at,成员函数,用于根据fd做索引,返回对应的tag值;

模板特化类型FdTable定义,指定静态数组大小为128。

using FdTable = FdTableImpl<128>;

全局变量fd_table定义在头文件bionic/libc/private/bionic_globals.h中

image
image

通过注释,可以知道libc_shared_globals结构体会在动态链接libc共享库时候得到构造,其中成员fd_table也就会被构造。

(3) 初始化

fdsan模块的初始化函数__libc_init_fdsan,完成fdsan特性属性debug.fdsan设置到内部数据fd_table中。

image
image

默认值为ANDROID_FDSAN_ERROR_LEVEL_FATAL,如果系统属性"debug.fdsan"有设置有效值,则使用读取的有效值对fd_talbe变量进行设定,否则使用默认值设定。

fdsan特性属性名kFdsanPropertyName 定义如下 static constexpr const char* kFdsanPropertyName = "debug.fdsan"; fdsan特性属性"debug.fdsan"可能取值定义如下:

image
image

__libc_init_fdsan会在libc的初始化流程中被调用到。调用点位于bionic/libc/bionic/libc_init_common.cpp文件中的__libc_init_common

image
image

从__libc_init_common的调用点,我们可以明白fdsan初始化生效逻辑。

bionic/libc/bionic/libc_init_dynamic.cpp文件中的__libc_preinit_impl;

bionic/libc/bionic/libc_init_static.cpp文件中的__real_libc_init。

可见不管是静态链接libc还是动态链接libc,只要是链接了libc库的进程,都会保证fdsan的初始化流程的执行。

下面从fdsan对外暴露的三个API来剖析fdsan的内部实现。

(4) android_fdsan_create_owner_tag

image
image

通过传入的type和tag字段,拼接成一个有效的close_tag值。

然后调用android_fdsan_exchange_owner_tag进行ownership的设定。

(5) android_fdsan_exchange_owner_tag

image
image

入参说明:

fd,fd句柄,作为FdEntry的索引

expected_tag,期望的ownership tag值

new_tag,设置新的ownership tag值

通过fd索引找到对应的FdEntry,判断tag值是否与expected_tag一致,一致说明ownership符合预期,可以使用new_tag值重新设定对应的FdEntry。

比较与设置操作通过原子操作atomic_compare_exchange_strong完成,可以保证是线程安全的。

如果不符合ownership预期,则说明检测到了异常,根据expected_tag和FdEntry tag关系调用fdsan_error进行错误处理。

(6) android_fdsan_close_with_tag

image
image

入参说明:

fd,待关闭的fd句柄

expected_tag,期望的ownership tag,如果与fd对应的FdEntry匹配,则执行正常关闭操作,否则,说明检测到异常,进行错误处理。

其中FDTRACK_CLOSE在fdtrack章节4.4.1进行介绍。

tag匹配检查操作也是通过原子操作atomic_compare_exchange_strong完成,保证线程安全。

如果close_tag和expected_tag相等,符合预期,可以继续调用__close执行关闭操作;

否则检测到异常,根据close_tag和expected_tag的关系,调用fdsan_error进行错误处理。错误处理相关代码足够清晰,就不用赘述了。

如果ownership匹配,但是调用__close返回失败,再进行判断,是否发生了double close。

(7) fdsan_error

image
image

根据设定的error_level ,进行异常处理。

如果是ANDROID_FDSAN_ERROR_LEVEL_DISABLED,do nothing;

如果是ANDROID_FDSAN_ERROR_LEVEL_WARN_ONCE,打印一次告警信息,然后重新设定error_level为ANDROID_FDSAN_ERROR_LEVEL_DISABLED

如果是ANDROID_FDSAN_ERROR_LEVEL_WARN_ALWAYS,总是打印告警信息;

如果是ANDROID_FDSAN_ERROR_LEVEL_FATAL,直接发送abort信号自杀。

四、fdtrack机制介绍

1. fdtrack简介

(1) fdtrack是什么

fdtrack是android在R版本开始引入,为进程fd资源泄露问题,提供一套统一的检测机制。

(2) fdtrack可以干什么

fdtrack使能后,可以检测进程fd资源泄露问题,检测到fd资源泄露后,可以打印出fd分配路径的调用栈,辅助问题的定位。

(3) fdtrack怎么用

fdtrack的实现方式中,固化了一部分桩代码到libc中,主要检测代码则实现在一个共享库libfdtrack中。要使用fdtrack功能的进程,需要动态的加载libfdtrack库来使能fdtrack功能。

android中给出了libfdtrack如何使用的示例代码:

在system_server进程中使能fdtrack检测功能,基本思路是,通过进程内已分配的fd资源的绝对数量来决定何时启用fdtrack功能,以及判定何时发生了fdleak。

创建一个monitor线程,周期性的检测进程fd资源是否超过了预定的阈值,当超过第一个检测trigger阈值时,主动加载libfdtrack库,使能fdtrack功能;当继续超过第二个泄露阈值时,会发送信号BIONIC_SIGNAL_FDTRACK,调用到libfdtrack库中信号处理函数进行异常处理。

下面让我们一下看一看,system_server使用fdtrack的具体示例代码吧。

在system_server主线程run函数中,调用spawnFdLeakCheckThread创建一个moniter线程,默认只在debug版本中打开。

代码语言:javascript
复制
if (Build.IS_DEBUGGABLE) {

    spawnFdLeakCheckThread();

}

创建monitor线程的实现,android R版本中是通过JNI接口调用native实现,android S版本中是直接实现在java端了,我们看一下S版本中的实现。

image
image

两个阈值可以通过属性重新设定,检测trigger阈值默认为1024,泄露阈值默认为2048,检测周期默认值设置为120秒。

当fd首次超过检测阈值时,动态加载libfdtrack共享库,使能fdtrack检测;

当fd超过泄露阈值时,调用fdtrackAbort进行异常处理。

其中有个小优化项:每个检测周期,如果fd超过检测阈值时,先尝试主动GC进行一次清理,看是否改善,如果清理完成后,fd还是超过检测阈值,就会走上面描述的逻辑。

2. 涉及代码路径

代码语言:javascript
复制
android/bionic/libc/platform/bionic/fdtrack.h

android/bionic/libc/private/bionic_fdtrack.h

android/bionic/libfdtrack/fdtrack.cpp

android/bionic/libc/bionic/fdtrack.cpp

其中fdtrack.h,是fdtrack方案的公共头文件,包含了API接口原型声明,fdtrack_event的定义以及fdtrack_event_type的枚举定义;

bionic_fdtrack.h,是fdtrack方案内部私有头文件,定义了fdtrack在libc里内部嵌入的包装宏定义FDTRACK_CREATE_NAME、FDTRACK_CLOSE;

libc/bionic/fdtrack.cpp,是fdtrack方案固化在libc部分的实现代码。包括初始化部分,以及钩子函数设置,以及线程内部fdtack使能设置函数;

libfdtrack/fdtrack.cpp,是fdtrack方案libfdtrack的实现部分。

3. 设计思路解读

fdtrack的设计思路也比较直观明了:

通过预先在libc代码中埋伏好钩子函数(所有文件打开相关的API接口已经预先埋好桩),通过FDTRACK_CREATE_NAME包装宏实现埋桩。

埋桩点是否生效,是通过钩子函数__android_fdtrack_hook是否有效来控制,而钩子函数又是通过libfdtrack共享库的加载来动态赋有效值的。

这就非常好的实现了动态控制。需要使用fdtrack功能的时候,动态加载libfdtrack库,设置有效钩子函数,激活埋桩点代码。否则埋桩点代码执行空语句,不增加运行负载。

加载libfdtrack共享库后,以后的每次文件打开操作,都会调用到fdtrack内部实现代码,进行调用栈记录;每次文件关闭操作,也会调用到fdtrack内部实现代码,进行调用栈移除操作;

最后如果发生了fd泄露,只需要打印出内部记录的调用栈信息,即可辅助fd泄露问题的分析定位。

4. 实现代码解读

(1) 预埋桩代码解读

钩子函数__android_fdtrack_hook定义在libc/bionic/fdtrack.cpp中。

_Atomic(android_fdtrack_hook_t) __android_fdtrack_hook;

是一个指向android_fdtrack_hook_t类型的原子类型变量,默认初始值为空指针,只有当libfdtrack加载后,才会被赋予有效值fd_hook。

文件打开与关闭API包装宏,功能与用法通过注释可以看到。

image
image

包装宏FDTRACK_CREATE_NAME可以自己指定name参数,包装参数到fdtrack_event,调用__android_fdtrack_hook处理fdtrack_event。

image
image

包装宏FDTRACK_CREATE直接使用包装宏的调用函数名作为name参数。

只有加载了libfdtrack共享库,才会将__android_fdtrack_hook设置为有效值,满足非空条件,执行hook函数,从而进入到fdtrack内部实现。

image
image

包装宏FDTRACK_CLOSE,仅包装参数到fdtrack_event,调用__android_fdtrack_hook处理fdtrack_event,完成fdtrack调用栈信息记录的闭环,不真实执行close操作。

FDTRACK_CREATE_NAME埋桩调用点,通过搜索代码,可以看到预先埋到了各个file descriptor creation API接口函数中

image
image

FDTRACK_CLOSE,在3.4.6节介绍fdsan的时候有提到过,埋桩到android_fdsan_close_with_tag函数中,在各个链接了libc的进程中执行file close时,保证都会调用到android_fdsan_close_with_tag。

预埋桩代码的初始化部分__libc_init_fdtrack同3.4.3节介绍的__libc_init_fdsan,不再赘述。可以保证不论是动态加载libc库,还是静态加载libc库,都会调用到fdtrack静态预埋装部分代码。

image
image

_libc_init_fdtrack在调用进程内部注册信号BIONIC_SIGNAL_FDTRACK,处理函数为空函数,只实现预先占位功能。后面讲到ctor初始化时,可以看到动态加载fdtrack库时,ctor初始化时,会重新注册信号BIONIC_SIGNAL_FDTRACK处理函数。

(2) fdtrack内部实现数据结构定义

首先介绍的是android_fdtrack_event结构体,封装了发送到fd_hook的数据包

image
image

其中有重要的成员type,用来在fd_hook中区分是create还是close事件。

image
image

fdtrack内部使用全局数组stack_traces记录调用栈信息,该静态数组大小为4096。

image
image

(3) 动态加载初始化与析构去初始化

libfdtrack库动态加载时,会调用到初始化函数ctor,这是通过属性__attribute__((constructor))设定的。

image
image

该函数会完成4件事情

1)初始化调用栈记录器stack_traces的backtrace成员,分配数组大小为kStackDepth;

2)注册信号BIONIC_SIGNAL_FDTRACK的处理函数fdtrack_dump/fdtrack_dump_fatal;

3)设置__android_fdtrack_hook为fd_hook;

4)设置fdtrack使能标志。

对应的去初始化dtor,主要做了一件事情,就是将__android_fdtrack_hook设定为nullptr。

image
image

(4) fd_hook实现

image
image

根据event的类型进行相应处理:

如果是create,以fd为索引,获取对应的FdEntry,并记录分配路径的调用栈;

如果是close,以fd为索引,获取对应的FdEntry,清除调用栈记录;

其中GetFdEntry,以fd为索引,返回指向对应的FdEntry元素的指针。

image
image

(5) fdtrack_dump实现

从前面ctor的分析可知,注册的BIONIC_SIGNAL_FDTRACK信号处理函数中,会根据发送的信号附加的siginfo信息,区分处理方式,是fatal模式还是非fatal模式。

其中使用sigqueue方式发送BIONIC_SIGNAL_FDTRACK信号,且携带了正确的siginfo时,会走到fdtrack_dump_fatal里。

image
image

可以看到两个不同的接口函数,调用到同一个实现函数fdtrack_dump_impl里,通过参数区分。

fdtrack_dump_impl的核心实现,包含以下几个部分:

1)内部定义一个128个元素的静态StackInfo数组,来对全局stack_traces中记录的调用栈,进行相同调用栈合并计数(通过hash判断是否相同调用栈),并输出最多次数的分配调用栈信息。

image
image

2)调用辅助函数fdtrack_iterate,对记录的调用栈信息进行合并与排序处理。

image
image

3)如果是fatal类型的,另起一个线程主动abort,通过注释,可以看到是为了避免ART dump runtime stack

image
image

fdtrack_iterate,实现了遍历全局调用栈数组stack_traces,对其中每一项有效调用栈记录backtrace,提取出函数name与函数内offset信息,然后调用callback接口进行计算处理。

完成相同调用栈计数合并,以及找到最大计数调用栈。

image
image

我们注意到通过在入口处调用android_fdtrack_set_enabled(false),出口处调用android_fdtrack_set_enabled(prev),来实现fdtrack_iterate函数的线程安全处理。

相同调用栈判断,是通过计算调用栈中所有函数name与函数内offset的hash方式来得出的。

image
image

五、我们可以从中学到什么

android中fdtrack的实现方式非常值得我们学习。

通过静态埋桩把基础代码固化到libc中,所有链接libc的程序,都会得到静态埋桩。然后通过动态加载库来设置前面静态埋桩的钩子函数为有效函数,从而达到动态使能某个特性的目的。

这种静态埋桩,外加动态库动态使能的设计方法值的我们学习。

六、进一步思考

1)GKI后,定制化feature如何实现?

android最近几个版本,都在强力推行GKI,完全采用GKI后,vendor和OEM厂家的kernel定制化代码实现方式只能有两种选择:

对于必须要实现在内核态的代码,选择ko化,需要严格遵守KMI约束;

对于可以上移到用户空间实现的代码,上移到bionic中实现。

而对于后一种方式,fdtrack方案的设计方法非常值得我们学习借鉴。

2)fdtrack还有没有其他的使用方式?

AOSP中针对fdtrack方案,给出的使用方式示例代码,为system_server创建一个monitor线程周期检测。

那么还有没有其他的使用方式呢?

在这里给大家抛出一个问题,大家可以结合自己的实际使用场景,思考一下。

参考资料:

1.《Unix环境高级编程》

2.《存储基础——文件描述符fd究竟是什么》https://www.qiyacloud.cn/2021/04/2021-04-07/

3.android S版本AOSP源代码 https://android.googlesource.com/platform/bionic/+/refs/heads/master

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-07-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、引言
  • 二、背景知识
    • 1. 什么是文件描述符
      • 2. 获取fd的时机
        • 3. fd取值范围的限制
          • 4. 内核如何使用fd管理进程打开的文件
            • (5) 文件描述符与文件关系
            • 三、fdsan机制介绍
              • 1. fdsan简介
                • 2. 涉及代码路径
                  • 3. 设计思路解读
                    • 4. 实现代码解读
                    • 四、fdtrack机制介绍
                      • 1. fdtrack简介
                        • 2. 涉及代码路径
                          • 3. 设计思路解读
                            • 4. 实现代码解读
                            • 五、我们可以从中学到什么
                            • 六、进一步思考
                            领券
                            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档