Linux中获取和释放fd的函数实现

文件描述符管理架构

核心数据结构关系

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_SETbtsl设置位单指令原子操作
__FD_CLRbtrl清除位单指令原子操作
__FD_ISSETbtl+setb检查位利用标志位高效判断
__FD_ZEROrep stosl清空位图批量清零,最高效

动态扩展机制 - 容量管理

expand_fdset vs expand_fd_array

方面expand_fdsetexpand_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

关键优化技术

  1. 快速搜索next_fd避免从0开始线性扫描
  2. 惰性扩展:按需动态扩展,不预分配过多资源
  3. 紧凑分配:总是分配最小的可用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_struct
    • current->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_bitnext_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)

关键设计要点

  1. 性能优化:使用 next_fd 避免每次都从0开始搜索
  2. 线程安全:使用自旋锁保护对文件描述符表的并发访问
  3. 动态扩展:支持在位图和数组不足时动态扩展容量
  4. 资源限制:严格遵守进程的文件描述符数量限制
  5. 默认行为:新分配的文件描述符默认不在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转换为整型

工作过程:

  1. 将文件描述符fd的值放入寄存器
  2. 执行btsl指令,将位图中对应fd的比特位设置为1
  3. 结果直接写回内存中的位图

宏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完全相同的内存和寄存器约束

工作过程:

  1. fd值放入寄存器
  2. 执行btrl指令,将位图中对应fd的比特位设置为0
  3. 结果写回内存

宏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=不在)

工作过程:

  1. 使用btl测试位图中fd对应的比特位
  2. 根据测试结果(CF标志)使用setb设置__result
  3. 返回__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":告诉编译器内存被修改,防止优化问题

工作过程:

  1. 设置EDI=位图地址,EAX=0,ECX=要清零的long数量
  2. 执行rep stosl,将整个位图区域用0填充
  3. 快速清空整个文件描述符集合

扩展文件描述符位图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_fdsclose_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):将新增部分初始化为0
    • memset(&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_opensetnew_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;

    • 返回错误码

关键设计特点

  1. 锁管理:精细的锁控制,在可能睡眠的内存分配期间释放锁
  2. 原子切换:使用 xchg 指令原子地更新指针,避免竞态条件
  3. 渐进扩展:采用页面大小起步,然后翻倍的扩展策略
  4. 错误安全:完善的错误处理,确保资源正确释放
  5. 内存效率:只拷贝有效数据,新增部分初始化为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 分配新的文件描述符数组
    • 大小为 nfdsstruct 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;

    • 统一的返回路径

关键设计特点

  1. 锁管理策略

    • 在内存分配期间释放锁,避免在可能睡眠的操作中持有自旋锁
    • 精细的锁控制确保并发安全
  2. 渐进式扩展

    • 采用256 → 页面大小 → 翻倍的扩展策略
    • 平衡内存使用和性能
  3. 原子指针交换

    • 使用 xchg 指令原子更新指针,避免竞态条件
    • 确保多线程环境下的数据一致性
  4. 竞态条件处理

    • 优雅处理"在睡眠期间数组已被扩展"的情况
    • 释放多余的内存并返回成功
  5. 内存效率

    • 只拷贝有效数据,新增部分清零
    • 及时释放旧数组避免内存泄漏

释放未使用文件描述符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_struct
  • current->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核心访问文件描述符表
  • 必须在修改完成后立即释放锁,以最小化锁竞争
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

---学无止境---

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值