LV05-03-Kernel-05-02-文件描述符分配

本文主要是kernel——文件描述符分配的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
PC端开发环境 Windows Windows11
Ubuntu Ubuntu20.04.2的64位版本
VMware® Workstation 17 Pro 17.6.0 build-24238078
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Win32DiskImager Win32DiskImager v1.0
Linux开发板环境 Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04
linux内核 linux-4.19.71(NXP官方提供)
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
nxp-imx/linux-imx/releases/tag/v4.19.71 NXP linux内核仓库tags中的v4.19.71
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)
Source Code https://elixir.bootlin.com/linux/latest/source linux kernel源码
https://elixir.bootlin.com/u-boot/latest/source uboot源码

一、linux内核文件相关结构体

img

1. struct files_struct

files_struct这个结构体定义在fdtable.h - include/linux/fdtable.h - files_struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
* Open file table structure
*/
struct files_struct {
/*
* read mostly part
*/
atomic_t count;
bool resize_in_progress;
wait_queue_head_t resize_wait;

struct fdtable __rcu *fdt;
struct fdtable fdtab;
/*
* written part on a separate cache line in SMP
*/
spinlock_t file_lock ____cacheline_aligned_in_smp;
unsigned int next_fd;
unsigned long close_on_exec_init[1];
unsigned long open_fds_init[1];
unsigned long full_fds_bits_init[1];
struct file __rcu * fd_array[NR_OPEN_DEFAULT];
};

进程打开文件的表结构,next_fd 表示下一个可用进程描述符,并不一定真正可用,假如 0-10 描述符都被使用了,中间释放3文件描述符,再打开文件,此时将使用3作为新的文件描述符,内核认为next_fd 为4,next_fd只是表示可能可用的下一个文件描述符,下次查找可用描述符时从next_fd开始查找,而不需要从头开始找。

2. struct fdtable

fdtable定义在fdtable.h - include/linux/fdtable.h - fdtable

1
2
3
4
5
6
7
8
struct fdtable {
unsigned int max_fds;
struct file __rcu **fd; /* current fd array */
unsigned long *close_on_exec;
unsigned long *open_fds;
unsigned long *full_fds_bits;
struct rcu_head rcu;
};

真正记录哪些文件描述符被使用了,哪些是空闲的,实际是一个文件描述符位图,每1bit表示了一个文件描述符,例如bit 0为1表示描述符1被使用了,bit 3为0表示描述符3可以使用。fd数组记录了file信息,数组下标就是文件描述符的值。

3. struct file

file结构体定义在fs.h - include/linux/fs.h - file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;

/*
* Protects f_ep_links, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
enum rw_hint f_write_hint;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
struct mutex f_pos_lock;
loff_t f_pos;
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;

u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;

#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
struct list_head f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
errseq_t f_wb_err;
} __randomize_layout
__attribute__((aligned(4))); /* lest something weird decides that 2 is OK */

struct file_handle {
__u32 handle_bytes;
int handle_type;
/* file identifier */
unsigned char f_handle[0];
};

file: 文件的真正信息,文件描述符只是个数组下标,通过下标查找file结构体信息,f_op记录的是文件读写及其他操作的真正函数,不同的文件系统,读写函数不一样,申请文件描述后,内核会将文件描述符与文件结构体(file读写函数等)关联起来。具体怎识别文件系统获取读写函数后面再学习。

二、文件描述符位图

1. fdtable

如下简要示意了下文件描述符位图结构:

img

前面已经提到,fdtable定义在fdtable.h - include/linux/fdtable.h - fdtable

1
2
3
4
5
6
7
8
struct fdtable {
unsigned int max_fds;
struct file __rcu **fd; /* current fd array */
unsigned long *close_on_exec;
unsigned long *open_fds;
unsigned long *full_fds_bits;
struct rcu_head rcu;
};

1.1 full_fds_bits

full_fds_bits每1bit代表的是一个32位的数组,也就是说代表了32位描述符;上面只画了32位,内核中的位图是一片连续的内存空间,最低bit表示数值0,下一比特表示1,依次类推;full_fds_bits每1bit只有0和1两个值,0表示有该组有可用的文件描述符,1表示没有可用的文件描述符,例如位图bit 0代表的是0-31共32个文件描述符,bit1代表的是32-63共32个文件描述符,假如0-31文件描述符都被使用了,那么位图bit0则应该标记为1,如果32-63中有一个未使用的文件描述符,则bit1被标记为0,当32-63中的所有文件描述符都被使用的时候,才标记为1。我们来跟踪一下full_fds_bits这个指针的调用关系(source insight中),图中紫色部分应该是进程创建过程中初始化的full_fds_bits这个参数。

image-20241209080006635

1.2 open_fds

open_fds是真正的文件描述符位图,也是一片连续的内存空间,每bit代表一个文件描述符(注意full_fds_bits每bit代表的是一组文件描述符),标记为0的bit表示该文件描述符没用被使用,标记为1的比特表示该文件描述符已经被使用,例如从内存其实地址开始计算,第35比特为1,则表示文件描述符35已经被使用了。

2. next zero bit查找函数

2.1 函数说明

这个函数是查找可用文件描述符,其实我是不确定到底用的是哪个的,在findbit.S - arch/arm/lib/findbit.S和这findbit.S - arch/unicore32/lib/findbit.S两个文件中都有相关的信息,但是这里都是汇编,就没有过于深究,大概了解一下吧。函数原型如下:

1
int find_next_zero_bit(void *addr, unsigned int maxbit, int offset)

参数:

  • addr位图内存起始地址;
  • maxbit最大bit偏移,也就是位图最后一bit的偏移;
  • offset起始查找地址,

前面有介绍位图数组full_fds_bits,通过该位图可以确定某组文件描述符里面是否有可以使用的文件描述符,另外next_fd也记录了下一个可能可用的文件描述符,因此查找可用文件描述符的时候,总是从可能可用的文件描述符开始查找,而不需要从头找,next_fd在打开和关闭文件描述符的时候会计算相关的内容。

2.2 基本原理

这里以这个findbit.S - arch/arm/lib/findbit.S - ENTRY(_find_next_zero_bit_le)为例来分析一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
* Purpose : Find next 'zero' bit
* Prototype: int find_next_zero_bit(void *addr, unsigned int maxbit, int offset)
*/
ENTRY(_find_next_zero_bit_le)
teq r1, #0
beq 3b
ands ip, r2, #7
beq 1b @ If new byte, goto old routine
ARM( ldrb r3, [r0, r2, lsr #3] )
THUMB( lsr r3, r2, #3 )
THUMB( ldrb r3, [r0, r3] )
eor r3, r3, #0xff @ now looking for a 1 bit
movs r3, r3, lsr ip @ shift off unused bits
bne .L_found
orr r2, r2, #7 @ if zero, then no bits here
add r2, r2, #1 @ align bit pointer
b 2b @ loop for next bit
ENDPROC(_find_next_zero_bit_le)

第 6 行:r1 = maxbit,如果maxbit为0,是不需要比较的。

第 8 行:判断offset低3位是否为0,查找0bit位的时候是以8位对齐查找的,3位二进制,如果offset是8的整数倍,那么低3位应该是0,跳转到1b处查找(从byte的第0位开始查找)

第 10 行:offset不是8的整数倍,那么先从offset % 8开始查找,假如offset = 18 = 15 + 3,0 -15 bit正好是2个字节,我们只需要从第3个字节的第3位开始查找即可,因为计算机读的时候是以最小单位字节读取的,所以我们不能直接读取第18bit,而是要读取16-23bit,相当于多读了3bit的值而已。

第 11 行:”lsr, #3” 实际是offset/8,获取的是bit对应的byte,例如18/8 = 2,表示18bit在内存的第2个字节里面(字节起始索引为0)

第 13 行:”eor r3, r3, #0xff”,这一行是8bit文件描述符进行异或操作,实际效果是各位取反,就是将0变1、1变0,对0的查找变为对1的查找,便于代码的编写

第 14 行:”movs r3, r3, lsr ip”,ip是offset % 8,右移文件描述符,就是将不需要比较的位移除(该函数是从指定位置开始找0bit位,但是并不是说指定位置之前都是1)

第 15 行:结果不为0,即有bit的值为1(前面已经将0取反为1了),找到了为1的bit则跳转到 .L_found

第 18 行:没有找到,继续查找,后续都是8个bit的查找。

再来看一下 .L_found

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/*
* One or more bits in the LSB of r3 are assumed to be set.
*/
.L_found:
#if __LINUX_ARM_ARCH__ >= 5
rsb r0, r3, #0
and r3, r3, r0
clz r3, r3
rsb r3, r3, #31
add r0, r2, r3
#else
@ r3是前面取的8bit文件描述符,r3 & 0x0f用来判断低4位是否有1,即可用描述符是否在r3的低4位里面
tst r3, #0x0f
@ 上一条指令结果为0,表示r3低4位没有可用的文件描述符,offset = offset + 4,在第4位之后继续查找
addeq r2, r2, #4
@ tst指令执行结果不为0,表示r3低4位有1(即有可用文件描述符),将r3左移4位,移位后低4位就都为0了,注意这里offset没有变化,执行者条指令之后,可用描述符都集中的r3的高4位了,只要从第4位开始查找为1的bit就可以了。
movne r3, r3, lsl #4

@ 从第4位开始查找
@ 判断第4或者5位是否为1
tst r3, #0x30
@ 第4、5位都不为1,则为1的bit位必定在第6或7位,偏移先加2,offset = offset + 2
addeq r2, r2, #2
@ 第4或者第5位有1,则先左移2两位,这步的offset没有修改
movne r3, r3, lsl #2

@ 从第6位开始查找
@ 判断第6位是否为0
tst r3, #0x40
@ 第6位为0,则为1的bit位一定在第7位,offset = offset + 1
addeq r2, r2, #1
@ 为1(之前为0的bit取反得到的)的bit偏移位置,即可用的文件描述符,r0是函数的返回值
mov r0, r2
#endif
cmp r1, r0 @ Clamp to maxbit
movlo r0, r1
ret lr

说实话,没看懂。这里举个简单的例子再解释下;例如r3的第0-3bit都为0,第7bit为1,offset起始值为0,需要查找到低7bit,先让offset = offset + 4,然后从第4位找为1的第7bit位,此时第4到第7位的偏移是3,我们只需要让offset再加3即可offset = offset + 3 = 4 + 3 = 7,也就是我们每次查找的起始位置变了;再假如r3的第3bit为1,offset起始值为0,将r3左移4位,此时第3bit将变为第7bit,但是offset还是为0,接着我们从第4bit开始查找为1的bit,第4到第7bit的偏移为3,offset = offset + 3 = 0 + 3 = 3,得到的结果是正确的。

总结一句话就是,移位操作是为了使后面代码查找时的起点都是一样的。

3. __alloc_fd()

__alloc_fd()函数是文件描述符分配,该函数仅分配了一个可用的文件描述符,文件描述符与文件操作函数的关联不在这里处理。它定义在file.c - fs/file.c - __alloc_fd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/*
* allocate a file descriptor, mark it busy.
*/
int __alloc_fd(struct files_struct *files,
unsigned start, unsigned end, unsigned flags)
{
unsigned int fd;
int error;
struct fdtable *fdt;

spin_lock(&files->file_lock);
repeat:
fdt = files_fdtable(files); // 获取文件描述符表
fd = start;
if (fd < files->next_fd)
fd = files->next_fd; // 默认传递的起始查找文件描述不一定有效,不在有效范围时使用next_fd作为起始查找值

if (fd < fdt->max_fds)
fd = find_next_fd(fdt, fd); // 起始查找文件描述符小于最大文件描述符,从当前文件描述符表中查找可用的文件描述符(max_fds表示已分配的文件描述符的数量,也就是位图总的bit数,后面会看到文件描述符表扩展的代码,在此先介绍下)

/*
* N.B. For clone tasks sharing a files structure, this test
* will limit the total number of files that can be opened.
*/
error = -EMFILE;
if (fd >= end) // 可用文件描述符超出函数参数传递的最大值,返回-EMFILE,这是个标准错误码errno
goto out;

error = expand_files(files, fd); // 扩展文件描述符,当fd<=max_fds时,fd在文件描述符位图可表示的范围,例如我们申请的文件描述符大小为1byte,那么文件描述符最大只能表示7,当fd大于7的时候,我们就没有对应的bit位可以标记了,因此需要重新扩展,申请更大的内存,申请的新的文件描述符表,让后将旧的值拷贝到新的文件描述符表中。只有fd>max_fds才会真正扩展。
if (error < 0)
goto out;

/*
* If we needed to expand the fs array we
* might have blocked - try again.
*/
if (error)
goto repeat;

if (start <= files->next_fd)
files->next_fd = fd + 1;

__set_open_fd(fd, fdt); // 在文件描述符表中标记fd已经被打开,对应bit位设置为1,同时更新fd所在文件描述符组的值,因为fd改变后,可能导致该组的文件描述符都被使用了,需要将该组标记为1,下次查找可用文件描述符时就会跳过该组,避免不必要的查找。
if (flags & O_CLOEXEC)
__set_close_on_exec(fd, fdt); // 打开时带有O_CLOEXEC标志,设置close_on_exec文件描述符位打开状态,大致意思是exec创建进程时会覆盖父进程,但是子进程继承了父进程的文件描述符,对于exec创建的新进程,继承的文件描述符已经没有任何意义了,创建之后需要关闭这些无意义的文件描述符,而这些文件描述符就记录在close_on_exec里面。
else
__clear_close_on_exec(fd, fdt);
error = fd;
#if 1
/* Sanity check */
if (rcu_access_pointer(fdt->fd[fd]) != NULL) {
printk(KERN_WARNING "alloc_fd: slot %d not NULL!\n", fd);
rcu_assign_pointer(fdt->fd[fd], NULL); // 文件操作函数设置为NULL,此时只分配了文件描述符,还没有真正关联到具体的文件操作函数
}
#endif

out:
spin_unlock(&files->file_lock);
return error;
}

4. find_next_fd()

find_next_fd()函数是查找下一个可用的文件描述符,它定义在file.c - fs/file.c - find_next_fd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static unsigned int find_next_fd(struct fdtable *fdt, unsigned int start)
{
unsigned int maxfd = fdt->max_fds; // 文件描述符表最大文件描述符
unsigned int maxbit = maxfd / BITS_PER_LONG; // 最大文件描述符组(一组文件描述符包含32个文件描述符,例如0-31为一组)
unsigned int bitbit = start / BITS_PER_LONG; // 起始查找文件描述符所在组(32个文件描述符为一组,我们要从文件描述符33开始查找,可知,33文件描述符在33/32 = 1组,因此我们从第1组开始查找即可)


bitbit = find_next_zero_bit(fdt->full_fds_bits, maxbit, bitbit) * BITS_PER_LONG; // 查找下一个可用文件描述符组,结果乘以BITS_PER_LONG,即得到该组起始文件描述符。
if (bitbit > maxfd)
return maxfd; // 可用文件描述符起始值大于最大文件描述符,直接返回最大文件描述符,表示文件描述符需要扩展。
if (bitbit > start)
start = bitbit; // 可用起始文件描述符大于参数传递的起始查找文件描述符,将开始查找的值从真正有效的值开始,避免做无效的查找。
return find_next_zero_bit(fdt->open_fds, maxfd, start); // 从文件描述符表中查找可用的文件描述符(之前是查找可用组,以32位为大小查找,提高效率,这次的查找范围缩小的组内了,即最多只需要查找32次了,这有点像找房间一样,先找楼层,再找房间,而不需要每层楼每间房间都找一遍;查找函数在前面章节已经介绍了)。
}

5. expand_files()

文件描述表大小是根据需要动态增加的,不会一开始就申请很大空间,这样会浪费内存,当文件描述符表不够大时才重新分配内存空间。expand_files()函数用于扩展文件描述符表。这个函数定义在file.c - fs/file.c - expand_files

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/*
* Expand files.
* This function will expand the file structures, if the requested size exceeds
* the current capacity and there is room for expansion.
* Return <0 error code on error; 0 when nothing done; 1 when files were
* expanded and execution may have blocked.
* The files->file_lock should be held on entry, and will be held on exit.
*/
static int expand_files(struct files_struct *files, unsigned int nr)
__releases(files->file_lock)
__acquires(files->file_lock)
{
struct fdtable *fdt;
int expanded = 0;

repeat:
fdt = files_fdtable(files); // 获取文件描述符表


/* Do we need to expand? */
if (nr < fdt->max_fds) // 新的文件描述符<max_fds,旧的文件描述符表已经足够表示该文件描述符了,不需要扩展。
return expanded;

/* Can we expand? */
if (nr >= sysctl_nr_open) // 大于限制的最大文件描述符,返回错误,不运行操作系统设置的最大文件描述符
return -EMFILE;

if (unlikely(files->resize_in_progress)) { // 同一进程下的多个线程是共用一个文件描述符的,需要互斥访问
spin_unlock(&files->file_lock);
expanded = 1;
wait_event(files->resize_wait, !files->resize_in_progress);
spin_lock(&files->file_lock);
goto repeat;
}

/* All good, so we try */
files->resize_in_progress = true;
expanded = expand_fdtable(files, nr); // 扩展文件描述符表
files->resize_in_progress = false;

wake_up_all(&files->resize_wait);
return expanded;
}

第38行,调用expand_fdtable()函数进行文件描述符表的扩展。

5.1 expand_fdtable()

expand_fdtable()函数用于文件描述符表的扩展,它定义在file.c - fs/file.c - expand_fdtable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
* Expand the file descriptor table.
* This function will allocate a new fdtable and both fd array and fdset, of
* the given size.
* Return <0 error code on error; 1 on successful completion.
* The files->file_lock should be held on entry, and will be held on exit.
*/
static int expand_fdtable(struct files_struct *files, unsigned int nr)
__releases(files->file_lock)
__acquires(files->file_lock)
{
struct fdtable *new_fdt, *cur_fdt;

spin_unlock(&files->file_lock);
new_fdt = alloc_fdtable(nr); // 申请足以表示nr文件描述符的内存空间
// ......
copy_fdtable(new_fdt, cur_fdt); // 文件描述符信息拷贝
rcu_assign_pointer(files->fdt, new_fdt); // 文件描述符表指针更新
if (cur_fdt != &files->fdtab)
call_rcu(&cur_fdt->rcu, free_fdtable_rcu);
/* coupled with smp_rmb() in __fd_install() */
smp_wmb();
return 1;
}

第 15 行,通过调用alloc_fdtable()函数来申请足以表示nr文件描述符的内存空间。

5.2 alloc_fdtable()

alloc_fdtable()函数定义在file.c - fs/file.c - alloc_fdtable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
static struct fdtable * alloc_fdtable(unsigned int nr)
{
struct fdtable *fdt;
void *data;

/*
* Figure out how many fds we actually want to support in this fdtable.
* Allocation steps are keyed to the size of the fdarray, since it
* grows far faster than any of the other dynamic data. We try to fit
* the fdarray into comfortable page-tuned chunks: starting at 1024B
* and growing in powers of two from there on.
*/
nr /= (1024 / sizeof(struct file *));
nr = roundup_pow_of_two(nr + 1);
nr *= (1024 / sizeof(struct file *)); // nr大小不确定,这之前的步骤就是为了调整nr大小,具体含义看英文说明
/*
* Note that this can drive nr *below* what we had passed if sysctl_nr_open
* had been set lower between the check in expand_files() and here. Deal
* with that in caller, it's cheaper that way.
*
* We make sure that nr remains a multiple of BITS_PER_LONG - otherwise
* bitmaps handling below becomes unpleasant, to put it mildly...
*/
if (unlikely(nr > sysctl_nr_open)) // nr大于系统设置的文件描述符上限,需要调整不超过系统设置的上限
nr = ((sysctl_nr_open - 1) | (BITS_PER_LONG - 1)) + 1;

fdt = kmalloc(sizeof(struct fdtable), GFP_KERNEL_ACCOUNT); // 申请内存空间
if (!fdt)
goto out;
fdt->max_fds = nr; // 最大文件描述符
data = kvmalloc_array(nr, sizeof(struct file *), GFP_KERNEL_ACCOUNT);
if (!data)
goto out_fdt;
fdt->fd = data;

data = kvmalloc(max_t(size_t,
2 * nr / BITS_PER_BYTE + BITBIT_SIZE(nr), L1_CACHE_BYTES),
GFP_KERNEL_ACCOUNT);
if (!data)
goto out_arr;
fdt->open_fds = data; // 文件描述符位图(每一位代表一个文件描述符)
data += nr / BITS_PER_BYTE;
fdt->close_on_exec = data; // close_on_exec文件描述符位图(用于在exec创建替换父进程时,确定哪些文件描述符需要关闭)
data += nr / BITS_PER_BYTE;
fdt->full_fds_bits = data; // 文件描述符组位图(每一位代表一个文件描述符组,一组文件描述符有32个文件描述符,当该组文件描述符都被使用了的时候,将该组对应的bit位设置为1,表示该组已经没有可用文件描述符了)


return fdt;

out_arr:
kvfree(fdt->fd);
out_fdt:
kfree(fdt);
out:
return NULL;
}

6. __put_unused_fd()

为了加速文件描述符查找,文件描述符分配/释放的时候都会更新next_fd,用来标志下一个可能可用的文件描述符,例如:我们刚申请到了文件描述符3,那么就代表3之前的文件描述符都被使用了(分配文件描述符是从小到大分配的),下一个可能可用的文件描述符应该大于等于4,当然文件描述符4有可能已经被使用了,但是我们不必查找文件描述符4之前的文件描述符,这样就提高了效率。另外,例如:0-9文件描述符都被使用了,next_fd=10,现在close文件描述符3,3<10,因此我们更新next_fd为3,这样我们就能保证next_fd始终是最小可能可用的文件描述符,不会造成查找时跳过可用文件描述符的情况。

实现上述功能的函数为__put_unused_fd(),它定义在file.c - fs/file.c - __put_unused_fd

1
2
3
4
5
6
7
static void __put_unused_fd(struct files_struct *files, unsigned int fd)
{
struct fdtable *fdt = files_fdtable(files);
__clear_open_fd(fd, fdt); // 清除文件描述符使用标记,同时清除该文件描述符所在组的标记,释放了一个文件描述符,该组至少有一个文件描述符可使用
if (fd < files->next_fd)
files->next_fd = fd; // 更新下一个可用的文件描述符(打开文件时会更新)
}

三、文件描述符关联

linux系统下任何设备都是文件,任何文件操作接口都是一样的,对于应用程序来说,用户只获取到文件描述符,要实现对文件操作,内核还需要知道文件描述符对应的真正设备是什么,怎么操作;ext2、ext4、socket、pipe各种文件的操作都不一样,具体由vfs统一封装了。

1. do_sys_open()

这里简单看一下do_sys_open()函数,他定义在open.c - fs/open.c - do_sys_open

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
struct open_flags op;
int fd = build_open_flags(flags, mode, &op);
struct filename *tmp;

if (fd)
return fd;

tmp = getname(filename);
if (IS_ERR(tmp))
return PTR_ERR(tmp);

fd = get_unused_fd_flags(flags); // 获取未使用的文件描述符
if (fd >= 0) {
struct file *f = do_filp_open(dfd, tmp, &op); // 打开文件,获取file结构体,文件操作的函数指针等,在此暂不做解释,可以参考之前pipe管道的文章,自己分析下pipe操作函数是怎么获取到的,pipe操作比物理文件系统操作简单了很多,分析起来更容易。
if (IS_ERR(f)) {
put_unused_fd(fd);
fd = PTR_ERR(f);
} else {
fsnotify_open(f);
fd_install(fd, f); // 文件描述符与文件结构体(真正的文件操作函数等)关联,实际就是设置fd对应的数组的值为f,在此不做详细解释。
}
}
putname(tmp);
return fd;
}

后面我们再详细学习吧。

参考资料

linux文件描述符分配实现详解(基于ARM处理器)_fdtable-CSDN博客

Linux创建进程的源码分析-CSDN博客