文件描述符管理架构
核心数据结构关系
struct files_struct
├── spinlock_t file_lock (并发控制)
├── int max_fds (fd数组大小)
├── int max_fdset (位图大小)
├── int next_fd (优化搜索起点)
├── struct file **fd (文件指针数组)
├── fd_set *open_fds (打开文件位图)
└── fd_set *close_on_exec (exec时关闭位图)
位图操作宏 - 底层原子操作
功能:提供对fd_set位图的原子级操作
| 宏 | 指令 | 功能 | 性能特点 |
|---|---|---|---|
__FD_SET | btsl | 设置位 | 单指令原子操作 |
__FD_CLR | btrl | 清除位 | 单指令原子操作 |
__FD_ISSET | btl+setb | 检查位 | 利用标志位高效判断 |
__FD_ZERO | rep stosl | 清空位图 | 批量清零,最高效 |
动态扩展机制 - 容量管理
expand_fdset vs expand_fd_array
| 方面 | expand_fdset | expand_fd_array |
|---|---|---|
| 管理对象 | open_fds, close_on_exec位图 | fd[]文件指针数组 |
| 扩展策略 | 页面大小→翻倍 | 256→页面大小→翻倍 |
| 数据迁移 | memcpy位图数据 | memcpy指针数组 |
| 原子切换 | xchg交换两个位图指针 | xchg交换数组指针和大小 |
共同设计模式:
// 1. 释放锁(内存分配可能睡眠)
spin_unlock(&files->file_lock);
// 2. 计算新大小(渐进式扩展)
do {
if (当前大小 < 阈值) 当前大小 = 阈值;
else 当前大小 *= 2;
} while (当前大小 <= 需求大小);
// 3. 分配新内存
new_mem = alloc_xxx(新大小);
// 4. 重新加锁并原子切换
spin_lock(&files->file_lock);
old_mem = xchg(&files->指针, new_mem);
// 5. 拷贝数据并释放旧内存
核心分配器 - get_unused_fd
分配算法流程
开始
↓
加锁
↓
从next_fd开始搜索零位 → 找到fd
↓
检查资源限制 → 超限? → 返回EMFILE
↓
检查位图容量 → 不足? → 扩展fdset → 成功? → 重新搜索
↓
检查数组容量 → 不足? → 扩展fd_array → 成功? → 重新搜索
↓
FD_SET标记已使用
↓
FD_CLR清除close_on_exec
↓
更新next_fd = fd + 1
↓
完整性检查
↓
解锁并返回fd
关键优化技术
- 快速搜索:
next_fd避免从0开始线性扫描 - 惰性扩展:按需动态扩展,不预分配过多资源
- 紧凑分配:总是分配最小的可用
fd,避免碎片
获取一个未使用的文件描述符get_unused_fd
int get_unused_fd(void)
{
struct files_struct * files = current->files;
int fd, error;
error = -EMFILE;
spin_lock(&files->file_lock);
repeat:
fd = find_next_zero_bit(files->open_fds->fds_bits,
files->max_fdset,
files->next_fd);
/*
* N.B. For clone tasks sharing a files structure, this test
* will limit the total number of files that can be opened.
*/
if (fd >= current->signal->rlim[RLIMIT_NOFILE].rlim_cur)
goto out;
/* Do we need to expand the fdset array? */
if (fd >= files->max_fdset) {
error = expand_fdset(files, fd);
if (!error) {
error = -EMFILE;
goto repeat;
}
goto out;
}
/*
* Check whether we need to expand the fd array.
*/
if (fd >= files->max_fds) {
error = expand_fd_array(files, fd);
if (!error) {
error = -EMFILE;
goto repeat;
}
goto out;
}
FD_SET(fd, files->open_fds);
FD_CLR(fd, files->close_on_exec);
files->next_fd = fd + 1;
#if 1
/* Sanity check */
if (files->fd[fd] != NULL) {
printk(KERN_WARNING "get_unused_fd: slot %d not NULL!\n", fd);
files->fd[fd] = NULL;
}
#endif
error = fd;
out:
spin_unlock(&files->file_lock);
return error;
}
函数功能概述
功能:在当前进程的文件描述符表中,寻找并分配一个最小的、未被使用的文件描述符编号
返回值:成功时返回一个可用的文件描述符编号(>=0),失败时返回一个负数错误码(通常是-EMFILE,表示文件描述符已达上限)
代码逐段分析
第1段:变量声明与初始化
int get_unused_fd(void)
{
struct files_struct * files = current->files;
int fd, error;
error = -EMFILE;
spin_lock(&files->file_lock);
struct files_struct * files = current->files;current是一个宏,指向当前正在运行的进程的task_structcurrent->files指向该进程的文件管理结构体files_struct,其中包含了文件描述符表、打开文件位图等信息
int fd, error;- 声明局部变量,
fd用于存储找到的文件描述符,error用于存储错误码
- 声明局部变量,
error = -EMFILE;- 预先将错误码设置为"文件描述符过多"(默认假设会失败)
spin_lock(&files->file_lock);- 获取文件结构的自旋锁,这是关键的一步,确保在多核环境下,对文件描述符表的操作是原子的,防止竞态条件
第2段:寻找未使用的文件描述符
repeat:
fd = find_next_zero_bit(files->open_fds->fds_bits,
files->max_fdset,
files->next_fd);
repeat:标签,用于在特定情况下重新开始搜索fd = find_next_zero_bit(files->open_fds->fds_bits, files->max_fdset, files->next_fd);- 这是核心的搜索逻辑:
files->open_fds->fds_bits:是一个位图(bitmap),每一位代表一个文件描述符的状态(1=已使用,0=未使用)files->max_fdset:位图能表示的最大文件描述符数量files->next_fd:上次分配的文件描述符+1,这是为了快速找到下一个可能可用的fd,避免每次都从0开始搜索find_next_zero_bit从next_fd位置开始,在位图中寻找第一个为0的位,返回其位置(即未使用的fd)
第3段:资源限制检查
if (fd >= current->signal->rlim[RLIMIT_NOFILE].rlim_cur)
goto out;
- 检查找到的
fd是否超过了进程的资源限制 current->signal->rlim[RLIMIT_NOFILE].rlim_cur:获取当前进程允许打开的最大文件数(软限制)- 如果
fd >=这个限制,说明没有可用的文件描述符了,直接跳转到out标签返回错误
第4段:扩展位图检查
/* Do we need to expand the fdset array? */
if (fd >= files->max_fdset) {
error = expand_fdset(files, fd);
if (!error) {
error = -EMFILE;
goto repeat;
}
goto out;
}
- 检查找到的
fd是否超出了当前位图的管理范围 files->max_fdset:当前位图能管理的最大fd数量- 如果
fd >= files->max_fdset,说明位图太小,需要扩展:- 调用
expand_fdset(files, fd)尝试扩展位图 - 如果扩展成功(
!error),设置错误码并跳回repeat重新搜索 - 如果扩展失败,跳转到
out返回错误
- 调用
第5段:扩展文件描述符数组检查
/*
* Check whether we need to expand the fd array.
*/
if (fd >= files->max_fds) {
error = expand_fd_array(files, fd);
if (!error) {
error = -EMFILE;
goto repeat;
}
goto out;
}
- 检查找到的
fd是否超出了文件描述符数组的管理范围 files->max_fds:当前文件描述符数组的大小files->fd[]:是实际的文件描述符数组,每个元素指向一个struct file- 如果
fd >= files->max_fds,需要扩展数组:- 调用
expand_fd_array(files, fd)尝试扩展 - 逻辑与上一段相同:成功则重试,失败则返回
- 调用
第6段:分配文件描述符
FD_SET(fd, files->open_fds);
FD_CLR(fd, files->close_on_exec);
files->next_fd = fd + 1;
- 关键步骤:正式分配找到的文件描述符
FD_SET(fd, files->open_fds);- 在位图中将对应
fd的位设置为1,标记为"已使用"
- 在位图中将对应
FD_CLR(fd, files->close_on_exec);- 在
close_on_exec位图中将对应位清0,默认该文件在execve()时不关闭
- 在
files->next_fd = fd + 1;- 更新
next_fd,为下一次分配提供优化的起始搜索位置
- 更新
第7段:完整性检查
#if 1
/* Sanity check */
if (files->fd[fd] != NULL) {
printk(KERN_WARNING "get_unused_fd: slot %d not NULL!\n", fd);
files->fd[fd] = NULL;
}
#endif
- 这是一个调试性的完整性检查
- 检查文件描述符数组的对应位置是否为NULL(因为这是未使用的
fd) - 如果不为NULL,打印警告信息并强制设置为NULL
- 用
#if 1包裹,表示这个检查默认启用
第8段:返回结果
error = fd;
out:
spin_unlock(&files->file_lock);
return error;
}
error = fd;- 将找到的
fd赋值给error变量(此时error从错误码变为有效的fd)
- 将找到的
out:标签处的清理工作:spin_unlock(&files->file_lock);:释放自旋锁,允许其他CPU核心操作文件描述符表return error;:返回结果(成功返回fd>= 0,失败返回错误码 < 0)
关键设计要点
- 性能优化:使用
next_fd避免每次都从0开始搜索 - 线程安全:使用自旋锁保护对文件描述符表的并发访问
- 动态扩展:支持在位图和数组不足时动态扩展容量
- 资源限制:严格遵守进程的文件描述符数量限制
- 默认行为:新分配的文件描述符默认不在exec时关闭
fd位图工具函数
#define __FD_SET(fd,fdsetp) \
__asm__ __volatile__("btsl %1,%0": \
"=m" (*(__kernel_fd_set *) (fdsetp)):"r" ((int) (fd)))
#define __FD_CLR(fd,fdsetp) \
__asm__ __volatile__("btrl %1,%0": \
"=m" (*(__kernel_fd_set *) (fdsetp)):"r" ((int) (fd)))
#define __FD_ISSET(fd,fdsetp) (__extension__ ({ \
unsigned char __result; \
__asm__ __volatile__("btl %1,%2 ; setb %0" \
:"=q" (__result) :"r" ((int) (fd)), \
"m" (*(__kernel_fd_set *) (fdsetp))); \
__result; }))
#define __FD_ZERO(fdsetp) \
do { \
int __d0, __d1; \
__asm__ __volatile__("cld ; rep ; stosl" \
:"=m" (*(__kernel_fd_set *) (fdsetp)), \
"=&c" (__d0), "=&D" (__d1) \
:"a" (0), "1" (__FDSET_LONGS), \
"2" ((__kernel_fd_set *) (fdsetp)) : "memory"); \
} while (0)
宏定义功能概述
功能:这些宏用于操作文件描述符集合(fd_set)的位图,实现对特定文件描述符的设置、清除、检查和清零操作
背景知识:
fd_set是一个位图数组,每个比特位代表一个文件描述符的状态(1=在集合中,0=不在集合中)- 这些宏使用x86汇编指令直接操作位图,提供最高效的性能
代码逐段分析
宏1:__FD_SET - 设置文件描述符到位图中
#define __FD_SET(fd,fdsetp) \
__asm__ __volatile__("btsl %1,%0": \
"=m" (*(__kernel_fd_set *) (fdsetp)):"r" ((int) (fd)))
汇编指令部分:
"btsl %1,%0":这是核心的位测试并设置指令btsl= Bit Test and Set Long (32位操作)%1= 第二个操作数(文件描述符fd)%0= 第一个操作数(位图内存地址)
输出操作数:
"=m" (*(__kernel_fd_set *) (fdsetp))=m表示这是一个内存操作数,会被修改(=表示输出)*(__kernel_fd_set *) (fdsetp)将fdsetp转换为内核fd_set类型并解引用
输入操作数:
"r" ((int) (fd))r表示使用通用寄存器(int) (fd)将fd转换为整型
工作过程:
- 将文件描述符
fd的值放入寄存器 - 执行
btsl指令,将位图中对应fd的比特位设置为1 - 结果直接写回内存中的位图
宏2:__FD_CLR - 从位图中清除文件描述符
#define __FD_CLR(fd,fdsetp) \
__asm__ __volatile__("btrl %1,%0": \
"=m" (*(__kernel_fd_set *) (fdsetp)):"r" ((int) (fd)))
汇编指令部分:
"btrl %1,%0":位测试并复位指令btrl= Bit Test and Reset Long- 功能与
btsl类似,但是将指定位清零
操作数部分:
- 与
__FD_SET完全相同的内存和寄存器约束
工作过程:
- 将
fd值放入寄存器 - 执行
btrl指令,将位图中对应fd的比特位设置为0 - 结果写回内存
宏3:__FD_ISSET - 检查文件描述符是否在位图中
#define __FD_ISSET(fd,fdsetp) (__extension__ ({ \
unsigned char __result; \
__asm__ __volatile__("btl %1,%2 ; setb %0" \
:"=q" (__result) :"r" ((int) (fd)), \
"m" (*(__kernel_fd_set *) (fdsetp))); \
__result; }))
结构部分:
__extension__ ({ ... }):GCC的语句表达式,允许将多条语句作为一个表达式使用unsigned char __result;声明局部变量存储结果
汇编指令部分:
"btl %1,%2 ; setb %0":两条指令btl= Bit Test Long - 测试指定位,结果存入CF(进位标志)setb= Set Byte if Below - 如果CF=1,将目标设置为1,否则为0
输出操作数:
"=q" (__result):q表示使用字节寄存器(AL, BL, CL, DL)
输入操作数:
"r" ((int) (fd)):fd值放入寄存器"m" (*(__kernel_fd_set *) (fdsetp)):位图内存地址
返回值:
__result;:返回测试结果(1=在位图中,0=不在)
工作过程:
- 使用
btl测试位图中fd对应的比特位 - 根据测试结果(CF标志)使用
setb设置__result - 返回__result值
宏4:__FD_ZERO - 清空整个位图
#define __FD_ZERO(fdsetp) \
do { \
int __d0, __d1; \
__asm__ __volatile__("cld ; rep ; stosl" \
:"=m" (*(__kernel_fd_set *) (fdsetp)), \
"=&c" (__d0), "=&D" (__d1) \
:"a" (0), "1" (__FDSET_LONGS), \
"2" ((__kernel_fd_set *) (fdsetp)) : "memory"); \
} while (0)
结构部分:
do { ... } while (0):标准的宏定义包装,确保在使用时像单个语句一样工作int __d0, __d1;:声明临时寄存器变量
汇编指令部分:
"cld ; rep ; stosl":三条指令cld= Clear Direction Flag - 清除方向标志,确保正向移动rep= Repeat - 重复执行后续指令ECX次stosl= Store String Long - 将EAX值存储到[EDI]并递增EDI
输出操作数:
"=m" (*(__kernel_fd_set *) (fdsetp)):修改的内存位置"=&c" (__d0):ECX寄存器,&表示早期破坏"=&D" (__d1):EDI寄存器,同样早期破坏
输入操作数:
"a" (0):EAX寄存器初始化为0(要存储的值)"1" (__FDSET_LONGS):ECX寄存器设置为要清零的long数量"2" ((__kernel_fd_set *) (fdsetp)):EDI寄存器设置为位图起始地址
Clobber列表:
"memory":告诉编译器内存被修改,防止优化问题
工作过程:
- 设置EDI=位图地址,EAX=0,ECX=要清零的long数量
- 执行
rep stosl,将整个位图区域用0填充 - 快速清空整个文件描述符集合
扩展文件描述符位图expand_fdset
int expand_fdset(struct files_struct *files, int nr)
__releases(file->file_lock)
__acquires(file->file_lock)
{
fd_set *new_openset = NULL, *new_execset = NULL;
int error, nfds = 0;
error = -EMFILE;
if (files->max_fdset >= NR_OPEN || nr >= NR_OPEN)
goto out;
nfds = files->max_fdset;
spin_unlock(&files->file_lock);
/* Expand to the max in easy steps */
do {
if (nfds < (PAGE_SIZE * 8))
nfds = PAGE_SIZE * 8;
else {
nfds = nfds * 2;
if (nfds > NR_OPEN)
nfds = NR_OPEN;
}
} while (nfds <= nr);
error = -ENOMEM;
new_openset = alloc_fdset(nfds);
new_execset = alloc_fdset(nfds);
spin_lock(&files->file_lock);
if (!new_openset || !new_execset)
goto out;
error = 0;
/* Copy the existing tables and install the new pointers */
if (nfds > files->max_fdset) {
int i = files->max_fdset / (sizeof(unsigned long) * 8);
int count = (nfds - files->max_fdset) / 8;
/*
* Don't copy the entire array if the current fdset is
* not yet initialised.
*/
if (i) {
memcpy (new_openset, files->open_fds, files->max_fdset/8);
memcpy (new_execset, files->close_on_exec, files->max_fdset/8);
memset (&new_openset->fds_bits[i], 0, count);
memset (&new_execset->fds_bits[i], 0, count);
}
nfds = xchg(&files->max_fdset, nfds);
new_openset = xchg(&files->open_fds, new_openset);
new_execset = xchg(&files->close_on_exec, new_execset);
spin_unlock(&files->file_lock);
free_fdset (new_openset, nfds);
free_fdset (new_execset, nfds);
spin_lock(&files->file_lock);
return 0;
}
/* Somebody expanded the array while we slept ... */
out:
spin_unlock(&files->file_lock);
if (new_openset)
free_fdset(new_openset, nfds);
if (new_execset)
free_fdset(new_execset, nfds);
spin_lock(&files->file_lock);
return error;
}
函数功能概述
功能:扩展进程的文件描述符位图(open_fds 和 close_on_exec)的容量,以容纳更多的文件描述符
背景:当进程需要分配的文件描述符编号超过了当前位图的管理范围时,需要调用此函数来动态扩展位图大小
代码逐段分析
第1段:函数声明和变量初始化
int expand_fdset(struct files_struct *files, int nr)
__releases(files->file_lock)
__acquires(files->file_lock)
{
fd_set *new_openset = NULL, *new_execset = NULL;
int error, nfds = 0;
error = -EMFILE;
if (files->max_fdset >= NR_OPEN || nr >= NR_OPEN)
goto out;
-
__releases(files->file_lock)和__acquires(files->file_lock):- 这是内核的锁注解,表示函数会释放并重新获取
files->file_lock自旋锁 - 用于静态代码分析工具(如Sparse)理解锁的语义
- 这是内核的锁注解,表示函数会释放并重新获取
-
fd_set *new_openset = NULL, *new_execset = NULL;:- 声明两个新的位图指针:
new_openset(用于open_fds)和new_execset(用于close_on_exec) - 初始化为NULL
- 声明两个新的位图指针:
-
error = -EMFILE;:- 预设错误码为"文件描述符过多"(默认假设会失败)
-
if (files->max_fdset >= NR_OPEN || nr >= NR_OPEN):- 检查是否已经达到或超过了系统允许的最大文件描述符限制(NR_OPEN)
files->max_fdset:当前位图能管理的最大fd数量nr:请求的文件描述符编号- 如果任一条件满足,说明无法继续扩展,直接跳转到错误处理
第2段:计算新的位图大小
nfds = files->max_fdset;
spin_unlock(&files->file_lock);
/* Expand to the max in easy steps */
do {
if (nfds < (PAGE_SIZE * 8))
nfds = PAGE_SIZE * 8;
else {
nfds = nfds * 2;
if (nfds > NR_OPEN)
nfds = NR_OPEN;
}
} while (nfds <= nr);
-
nfds = files->max_fdset;:- 从当前大小开始计算新的容量
-
spin_unlock(&files->file_lock);:- 关键操作:释放文件锁,因为后续的内存分配可能睡眠,不能在持有自旋锁的情况下进行
-
do { ... } while (nfds <= nr);:- 循环计算新的容量,直到能够容纳请求的
nr文件描述符
- 循环计算新的容量,直到能够容纳请求的
-
if (nfds < (PAGE_SIZE * 8)):- 如果当前大小小于一个页面能容纳的位数(PAGE_SIZE * 8 = 一个页面的比特数)
- 则直接扩展到页面大小,这是为了高效利用内存
-
else { nfds = nfds * 2; ... }:- 否则采用翻倍策略进行扩展
- 如果翻倍后超过系统限制 NR_OPEN,则截断为 NR_OPEN
第3段:分配新的位图
error = -ENOMEM;
new_openset = alloc_fdset(nfds);
new_execset = alloc_fdset(nfds);
spin_lock(&files->file_lock);
if (!new_openset || !new_execset)
goto out;
-
error = -ENOMEM;:- 将错误码改为"内存不足",因为接下来要进行内存分配
-
new_openset = alloc_fdset(nfds);和new_execset = alloc_fdset(nfds);:- 分配两个新的位图,大小都为
nfds比特 alloc_fdset是内核内部函数,用于分配文件描述符位图
- 分配两个新的位图,大小都为
-
spin_lock(&files->file_lock);:- 重新获取文件锁,因为要修改共享的文件结构
-
if (!new_openset || !new_execset):- 检查内存分配是否成功
- 如果任一分配失败,跳转到错误处理
第4段:数据迁移和原子切换
error = 0;
/* Copy the existing tables and install the new pointers */
if (nfds > files->max_fdset) {
int i = files->max_fdset / (sizeof(unsigned long) * 8);
int count = (nfds - files->max_fdset) / 8;
/*
* Don't copy the entire array if the current fdset is
* not yet initialised.
*/
if (i) {
memcpy (new_openset, files->open_fds, files->max_fdset/8);
memcpy (new_execset, files->close_on_exec, files->max_fdset/8);
memset (&new_openset->fds_bits[i], 0, count);
memset (&new_execset->fds_bits[i], 0, count);
}
-
error = 0;:- 到这里说明操作成功,设置错误码为0
-
if (nfds > files->max_fdset):- 再次确认新的大小确实大于当前大小(防止竞态条件)
-
int i = files->max_fdset / (sizeof(unsigned long) * 8);:- 计算旧位图在
new_openset数组中的结束位置(以unsigned long为单位) sizeof(unsigned long) * 8是一个unsigned long的比特数
- 计算旧位图在
-
int count = (nfds - files->max_fdset) / 8;:- 计算需要清零的新增部分的字节数
-
if (i) { ... }:- 只有当旧位图不为空时才进行拷贝
memcpy(new_openset, files->open_fds, files->max_fdset/8):拷贝旧的打开文件位图数据memcpy(new_execset, files->close_on_exec, files->max_fdset/8):拷贝旧的close_on_exec位图数据memset(&new_openset->fds_bits[i], 0, count):将新增部分初始化为0memset(&new_execset->fds_bits[i], 0, count):同上
第5段:原子替换和资源清理
nfds = xchg(&files->max_fdset, nfds);
new_openset = xchg(&files->open_fds, new_openset);
new_execset = xchg(&files->close_on_exec, new_execset);
spin_unlock(&files->file_lock);
free_fdset (new_openset, nfds);
free_fdset (new_execset, nfds);
spin_lock(&files->file_lock);
return 0;
}
-
nfds = xchg(&files->max_fdset, nfds);:- 原子地交换新旧
max_fdset值,返回旧值 - 使用原子操作防止竞态条件
- 原子地交换新旧
-
new_openset = xchg(&files->open_fds, new_openset);:- 原子地替换
open_fds指针,返回旧指针
- 原子地替换
-
new_execset = xchg(&files->close_on_exec, new_execset);:- 原子地替换close_on_exec指针,返回旧指针
-
spin_unlock(&files->file_lock);:- 释放锁,因为内存释放可能睡眠
-
free_fdset(new_openset, nfds);和free_fdset(new_execset, nfds);:- 释放旧的位图内存
- 注意:这里的
new_openset和new_execset实际上持有的是旧的指针(由于xchg交换)
-
spin_lock(&files->file_lock);和return 0;:- 重新获取锁并返回成功
第6段:错误处理路径
out:
spin_unlock(&files->file_lock);
if (new_openset)
free_fdset(new_openset, nfds);
if (new_execset)
free_fdset(new_execset, nfds);
spin_lock(&files->file_lock);
return error;
}
-
spin_unlock(&files->file_lock);:- 在错误路径上也要释放锁
-
if (new_openset) free_fdset(new_openset, nfds);:- 如果已经分配了新的
open_fds位图,释放它
- 如果已经分配了新的
-
if (new_execset) free_fdset(new_execset, nfds);:- 如果已经分配了新的close_on_exec位图,释放它
-
spin_lock(&files->file_lock);:- 重新获取锁,保持调用前后的锁状态一致
-
return error;:- 返回错误码
关键设计特点
- 锁管理:精细的锁控制,在可能睡眠的内存分配期间释放锁
- 原子切换:使用
xchg指令原子地更新指针,避免竞态条件 - 渐进扩展:采用页面大小起步,然后翻倍的扩展策略
- 错误安全:完善的错误处理,确保资源正确释放
- 内存效率:只拷贝有效数据,新增部分初始化为0
扩展文件描述符数组expand_fd_array
int expand_fd_array(struct files_struct *files, int nr)
__releases(files->file_lock)
__acquires(files->file_lock)
{
struct file **new_fds;
int error, nfds;
error = -EMFILE;
if (files->max_fds >= NR_OPEN || nr >= NR_OPEN)
goto out;
nfds = files->max_fds;
spin_unlock(&files->file_lock);
/*
* Expand to the max in easy steps, and keep expanding it until
* we have enough for the requested fd array size.
*/
do {
#if NR_OPEN_DEFAULT < 256
if (nfds < 256)
nfds = 256;
else
#endif
if (nfds < (PAGE_SIZE / sizeof(struct file *)))
nfds = PAGE_SIZE / sizeof(struct file *);
else {
nfds = nfds * 2;
if (nfds > NR_OPEN)
nfds = NR_OPEN;
}
} while (nfds <= nr);
error = -ENOMEM;
new_fds = alloc_fd_array(nfds);
spin_lock(&files->file_lock);
if (!new_fds)
goto out;
/* Copy the existing array and install the new pointer */
if (nfds > files->max_fds) {
struct file **old_fds;
int i;
old_fds = xchg(&files->fd, new_fds);
i = xchg(&files->max_fds, nfds);
/* Don't copy/clear the array if we are creating a new
fd array for fork() */
if (i) {
memcpy(new_fds, old_fds, i * sizeof(struct file *));
/* clear the remainder of the array */
memset(&new_fds[i], 0,
(nfds-i) * sizeof(struct file *));
spin_unlock(&files->file_lock);
free_fd_array(old_fds, i);
spin_lock(&files->file_lock);
}
} else {
/* Somebody expanded the array while we slept ... */
spin_unlock(&files->file_lock);
free_fd_array(new_fds, nfds);
spin_lock(&files->file_lock);
}
error = 0;
out:
return error;
}
函数功能概述
功能:扩展进程的文件描述符数组(files->fd[])的容量,以容纳更多的文件描述符条目
背景:当进程需要分配的文件描述符编号超过了当前文件描述符数组的大小时,需要调用此函数来动态扩展数组容量
代码逐段分析
第1段:函数声明和初始检查
int expand_fd_array(struct files_struct *files, int nr)
__releases(files->file_lock)
__acquires(files->file_lock)
{
struct file **new_fds;
int error, nfds;
error = -EMFILE;
if (files->max_fds >= NR_OPEN || nr >= NR_OPEN)
goto out;
-
__releases(files->file_lock)和__acquires(files->file_lock):- 锁注解,表明这个函数会临时释放并重新获取文件锁
- 这是因为内存分配操作可能睡眠,不能在持有自旋锁的情况下进行
-
struct file **new_fds;:- 声明新的文件描述符数组指针
struct file **表示指向struct file指针的指针,即文件指针数组
-
int error, nfds;:error:错误码nfds:新的文件描述符数组大小
-
error = -EMFILE;:- 预设错误码为"文件描述符过多"(Too many open files)
-
if (files->max_fds >= NR_OPEN || nr >= NR_OPEN):- 检查是否已经达到系统限制:
files->max_fds:当前数组大小nr:请求的文件描述符编号NR_OPEN:系统允许的最大文件描述符数
- 如果任一条件满足,直接跳转到错误处理
- 检查是否已经达到系统限制:
第2段:计算新的数组大小
nfds = files->max_fds;
spin_unlock(&files->file_lock);
/*
* Expand to the max in easy steps, and keep expanding it until
* we have enough for the requested fd array size.
*/
do {
#if NR_OPEN_DEFAULT < 256
if (nfds < 256)
nfds = 256;
else
#endif
if (nfds < (PAGE_SIZE / sizeof(struct file *)))
nfds = PAGE_SIZE / sizeof(struct file *);
else {
nfds = nfds * 2;
if (nfds > NR_OPEN)
nfds = NR_OPEN;
}
} while (nfds <= nr);
-
nfds = files->max_fds;:- 从当前大小开始计算
-
spin_unlock(&files->file_lock);:- 关键操作:释放文件锁,为可能睡眠的内存分配做准备
-
do { ... } while (nfds <= nr);:- 循环计算新的容量,直到能够容纳请求的
nr文件描述符
- 循环计算新的容量,直到能够容纳请求的
-
#if NR_OPEN_DEFAULT < 256条件编译:- 如果系统默认的文件描述符限制小于256
if (nfds < 256) nfds = 256;:至少扩展到256
-
if (nfds < (PAGE_SIZE / sizeof(struct file *))):- 计算一个页面能容纳多少个
struct file *指针 - 如果当前大小小于页面容量,直接扩展到页面大小
- 这是为了高效的内存使用
- 计算一个页面能容纳多少个
-
else { nfds = nfds * 2; ... }:- 否则采用翻倍策略
- 如果翻倍后超过系统限制,截断为 NR_OPEN
第3段:分配新数组
error = -ENOMEM;
new_fds = alloc_fd_array(nfds);
spin_lock(&files->file_lock);
if (!new_fds)
goto out;
-
error = -ENOMEM;:- 将错误码改为"内存不足",因为接下来进行内存分配
-
new_fds = alloc_fd_array(nfds);:- 调用
alloc_fd_array分配新的文件描述符数组 - 大小为
nfds个struct file *指针
- 调用
-
spin_lock(&files->file_lock);:- 重新获取文件锁,因为要修改共享的文件结构
-
if (!new_fds):- 检查内存分配是否成功
- 如果失败,跳转到错误处理
第4段:数据迁移和原子切换
/* Copy the existing array and install the new pointer */
if (nfds > files->max_fds) {
struct file **old_fds;
int i;
old_fds = xchg(&files->fd, new_fds);
i = xchg(&files->max_fds, nfds);
-
if (nfds > files->max_fds):- 确认新的大小确实大于当前大小
-
old_fds = xchg(&files->fd, new_fds);:- 原子操作:交换新旧文件描述符数组指针
- 将
files->fd设置为new_fds,返回旧的指针到old_fds
-
i = xchg(&files->max_fds, nfds);:- 原子操作:交换数组大小
- 将
files->max_fds设置为nfds,返回旧的大小到i
第5段:数据拷贝和资源清理
/* Don't copy/clear the array if we are creating a new
fd array for fork() */
if (i) {
memcpy(new_fds, old_fds, i * sizeof(struct file *));
/* clear the remainder of the array */
memset(&new_fds[i], 0,
(nfds-i) * sizeof(struct file *));
spin_unlock(&files->file_lock);
free_fd_array(old_fds, i);
spin_lock(&files->file_lock);
}
-
if (i):- 检查旧数组大小是否不为0
- 如果为0,说明是新建的数组(如在fork时),不需要拷贝数据
-
memcpy(new_fds, old_fds, i * sizeof(struct file *));:- 将旧数组中的数据拷贝到新数组
- 拷贝大小为旧数组的实际数据量
-
memset(&new_fds[i], 0, (nfds-i) * sizeof(struct file *));:- 将新数组的剩余部分清零初始化
- 确保新增的数组元素为NULL
-
spin_unlock(&files->file_lock);:- 释放锁,因为内存释放可能睡眠
-
free_fd_array(old_fds, i);:- 释放旧的数组内存
-
spin_lock(&files->file_lock);:- 重新获取锁
第6段:竞态条件处理和成功返回
} else {
/* Somebody expanded the array while we slept ... */
spin_unlock(&files->file_lock);
free_fd_array(new_fds, nfds);
spin_lock(&files->file_lock);
}
error = 0;
out:
return error;
}
-
} else { ... }:- 处理竞态条件:在我们睡眠分配内存期间,其他线程可能已经扩展了数组
- 此时
nfds <= files->max_fds,新分配的数组不再需要
-
spin_unlock(&files->file_lock);:- 释放锁以便安全释放内存
-
free_fd_array(new_fds, nfds);:- 释放刚刚分配但不再需要的新数组
-
spin_lock(&files->file_lock);:- 重新获取锁
-
error = 0;:- 设置成功返回码
- 注意:在竞态条件情况下,虽然我们释放了新数组,但仍然返回成功,因为数组已经被其他线程扩展了
-
out:标签和return error;:- 统一的返回路径
关键设计特点
-
锁管理策略:
- 在内存分配期间释放锁,避免在可能睡眠的操作中持有自旋锁
- 精细的锁控制确保并发安全
-
渐进式扩展:
- 采用256 → 页面大小 → 翻倍的扩展策略
- 平衡内存使用和性能
-
原子指针交换:
- 使用
xchg指令原子更新指针,避免竞态条件 - 确保多线程环境下的数据一致性
- 使用
-
竞态条件处理:
- 优雅处理"在睡眠期间数组已被扩展"的情况
- 释放多余的内存并返回成功
-
内存效率:
- 只拷贝有效数据,新增部分清零
- 及时释放旧数组避免内存泄漏
释放未使用文件描述符put_unused_fd
static inline void __put_unused_fd(struct files_struct *files, unsigned int fd)
{
__FD_CLR(fd, files->open_fds);
if (fd < files->next_fd)
files->next_fd = fd;
}
void fastcall put_unused_fd(unsigned int fd)
{
struct files_struct *files = current->files;
spin_lock(&files->file_lock);
__put_unused_fd(files, fd);
spin_unlock(&files->file_lock);
}
函数功能概述
功能:当一个文件描述符分配后但尚未与具体文件关联之前,如果由于某种原因需要取消分配,使用这两个函数将文件描述符标记为未使用状态,并返回到可用池中
典型场景:在 open 系统调用中,先调用 get_unused_fd 获取fd,如果文件打开失败,则调用 put_unused_fd 释放该fd
代码逐段分析
函数1:__put_unused_fd - 核心逻辑函数
static inline void __put_unused_fd(struct files_struct *files, unsigned int fd)
{
__FD_CLR(fd, files->open_fds);
if (fd < files->next_fd)
files->next_fd = fd;
}
第1行:函数声明
static inline void __put_unused_fd(struct files_struct *files, unsigned int fd)
static:限制函数作用域在当前文件inline:建议编译器内联展开,避免函数调用开销(这是性能关键路径)void:无返回值struct files_struct *files:进程的文件管理结构指针unsigned int fd:要释放的文件描述符编号
第2行:清除位图标记
__FD_CLR(fd, files->open_fds);
__FD_CLR(fd, files->open_fds):使用我们之前分析过的位操作宏- 将
files->open_fds位图中对应fd的比特位清零 - 这表示该文件描述符现在处于"未使用"状态,可以被重新分配
- 效果:在位图中将
fd标记为可用
第3-4行:优化next_fd指针
if (fd < files->next_fd)
files->next_fd = fd;
files->next_fd:记录下一次分配fd时的起始搜索位置if (fd < files->next_fd):如果释放的fd比当前next_fd更小files->next_fd = fd:将next_fd更新为这个更小的fd值
函数2:put_unused_fd - 对外接口函数
void fastcall put_unused_fd(unsigned int fd)
{
struct files_struct *files = current->files;
spin_lock(&files->file_lock);
__put_unused_fd(files, fd);
spin_unlock(&files->file_lock);
}
第1行:函数声明和属性
void fastcall put_unused_fd(unsigned int fd)
fastcall:这是一个调用约定,表示函数通过寄存器传递参数,而不是通过堆栈,以提高性能unsigned int fd:要释放的文件描述符编号
第2行:获取当前进程的文件结构
struct files_struct *files = current->files;
current:内核宏,指向当前进程的task_structcurrent->files:获取当前进程的文件管理结构- 这包含了文件描述符表、打开文件位图等信息
第3行:获取自旋锁
spin_lock(&files->file_lock);
- 关键操作:获取文件结构的自旋锁
- 确保对文件描述符表的修改是原子的,防止多线程竞态条件
第4行:调用核心逻辑
__put_unused_fd(files, fd);
- 调用我们刚才分析的
__put_unused_fd函数 - 传递文件结构指针和要释放的
fd - 执行实际的位图清除和
next_fd优化
第5行:释放自旋锁
spin_unlock(&files->file_lock);
- 关键操作:释放文件锁
- 允许其他线程或CPU核心访问文件描述符表
- 必须在修改完成后立即释放锁,以最小化锁竞争

1006

被折叠的 条评论
为什么被折叠?



