问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

存储基础 — 文件描述符 fd 究竟是什么?

创作时间:
作者:
@小白创作中心

存储基础 — 文件描述符 fd 究竟是什么?

引用
CSDN
1.
https://blog.csdn.net/armlinuxww/article/details/119600762

文件描述符(File Descriptor,简称fd)是操作系统中用于标识打开文件的关键概念。本文将深入探讨fd的本质、其在Linux内核中的实现原理,以及与之相关的进程管理、文件操作等核心机制。

前情概要

通过上一篇《Go存储基础 — 文件IO的姿势》,我们了解到有两种文件读写方式:一种是直接使用系统调用,操作的对象是一个整数fd;另一种是使用Go标准库封装的IO操作,操作对象是Go封装的file结构体,但其内部还是基于整数fd进行操作。因此,一切IO操作的根本都是通过fd来实现的。那么,这个fd究竟是什么呢?让我们深入剖析。

fd是什么?

fd是File Descriptor的缩写,中文名叫做文件描述符文件描述符是一个非负整数,本质上是一个索引值(这句话非常重要)。

什么时候拿到的fd?

当打开一个文件时,内核会向进程返回一个文件描述符(通过open系统调用得到)。后续对这个文件的读写操作(如readwrite),只需要使用这个文件描述符来标识该文件,将其作为参数传入即可。

fd的值范围是什么?

在POSIX语义中,0、1、2这三个fd值已经被赋予特殊含义,分别对应标准输入(STDIN_FILENO)、标准输出(STDOUT_FILENO)和标准错误(STDERR_FILENO)。

文件描述符的范围是0~OPEN_MAX-1。早期的UNIX系统中,这个范围很小,但现在的主流系统中,这个值几乎不受限制,只受到系统硬件配置和系统管理员配置的约束。

你可以通过ulimit命令查看当前系统的配置:

➜ ulimit -n
4864

如上,我系统上进程默认最多可以打开4864个文件。

窥探Linux内核

要理解fd究竟是什么,必须深入Linux内核。

task_struct

我们知道进程的抽象是基于struct task_struct结构体的,这是Linux中最复杂的结构体之一,包含了许多成员字段。我们今天重点关注其中的files字段:

struct task_struct {
    // ...
    /* Open file information: */
    struct files_struct     *files;
    // ...
}

files这个字段是今天的主角之一,它是一个指针,指向一个struct files_struct结构体。这个结构体用于管理该进程打开的所有文件。

files_struct

通过进程结构体引出了struct files_struct这个结构体。这个结构体用于管理某进程打开的所有文件,其结构如下:

/*
 * Open file table structure
 */
struct files_struct {
    // 读相关字段
    atomic_t count;
    bool resize_in_progress;
    wait_queue_head_t resize_wait;
    // 打开的文件管理结构
    struct fdtable __rcu *fdt;
    struct fdtable fdtab;
    // 写相关字段
    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 * fd_array[NR_OPEN_DEFAULT];
};

files_struct本质上是通过数组来管理所有打开的文件的。这里有两种数组:

  1. struct file * fd_array[NR_OPEN_DEFAULT]是一个静态数组,在64位系统上大小为64。
  2. struct fdtable是一个动态数组,其边界由max_fds字段描述。

这种静态+动态的方式是性能和资源的权衡:大部分进程只会打开少量文件,所以静态数组足够;如果超过静态数组的阈值,则动态扩展。

fdtable

简单介绍下fdtable结构体,这个结构体用于管理fd,其简化结构如下:

struct fdtable {
    unsigned int max_fds;
    struct file __rcu **fd;      /* current fd array */
};

注意到fdtable.fd是一个二级指针,指向一个数组,数组元素类型为struct file *max_fds指明数组边界。

file

现在我们知道了fd本质是数组索引,数组元素是struct file结构体的指针。这个结构体用于表征进程打开的文件,其简化结构如下:

struct file {
    // ...
    struct path                 f_path;
    struct inode                *f_inode;
    const struct file_operations *f_op;
    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;
    // ...
}

这个结构体非常重要,它标识一个进程打开的文件。其中:

  • f_path标识文件名
  • f_inode指向VFS的inode
  • f_pos表示当前文件偏移

inode

struct file结构体中有一个inode指针,这引出了VFS(虚拟文件系统)的概念。VFS抽象出了一层统一的inode概念,屏蔽了具体文件系统的差异。

完整架构图如下:

VFS的inode结构如下:

struct inode {
    // 文件相关的基本信息(权限,模式,uid,gid等)
    umode_t         i_mode;
    unsigned short  i_opflags;
    kuid_t          i_uid;
    kgid_t          i_gid;
    unsigned int    i_flags;
    // 回调函数
    const struct inode_operations   *i_op;
    struct super_block              *i_sb;
    struct address_space            *i_mapping;
    // 文件大小,atime,ctime,mtime等
    loff_t         i_size;
    struct timespec64   i_atime;
    struct timespec64   i_mtime;
    struct timespec64   i_ctime;
    // 回调函数
    const struct file_operations    *i_fop;
    struct address_space            i_data;
    // 指向后端具体文件系统的特殊数据
    void    *i_private;     /* fs or device private pointer */
};

VFS和后端具体文件系统的纽带是i_private字段,用于传递一些具体文件系统使用的数据结构。

思考问题

  1. files_struct结构体只会属于一个进程,那么struct file这个结构体呢,是只会属于某一个进程?还是可能被多个进程共享?

划重点:struct file是属于系统级别的结构,换句话说是可以共享与多个不同的进程。

  1. 什么时候会出现多个进程的fd指向同一个file结构体?

比如fork的时候,父进程打开了文件,后面fork出一个子进程。这种情况就会出现共享file的场景。

  1. 在同一个进程中,多个fd可能指向同一个file结构吗?

可以。dup函数就是做这个的。

#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);

思考实验

现在我们已经彻底了解fd这个所谓的非负整数代表的深层含义了,我们可以准备一些IO的思考举一反三。

文件读写(IO)的时候会发生什么?

  • 在完成write操作后,在文件file中的当前文件偏移量会增加所写入的字节数,如果这导致当前文件偏移量超处了当前文件长度,则会把inode的当前长度设置为当前文件偏移量(也就是文件变长)
  • O_APPEND标志打开一个文件,则相应的标识会被设置到文件file状态的标识中,每次对这种具有追加写标识的文件执行write操作的时候,file的当前文件偏移量首先会被设置成inode结构体中的文件长度,这就使得每次写入的数据都追加到文件的当前尾端处(该操作对用户态提供原子语义);
  • 若一个文件seek定位到文件当前的尾端,则file中的当前文件偏移量设置成inode的当前文件长度;
  • seek函数值修改file中的当前文件偏移量,不进行任何I/O操作;
  • 每个进程对有它自己的file,其中包含了当前文件偏移,当多个进程写同一个文件的时候,由于一个文件IO最终只会是落到全局的一个inode上,这种并发场景则可能产生用户不可预期的结果;

总结

回到初心,理解fd的概念有什么用?

一切IO的行为到系统层面都是以fd的形式进行。无论是C/C++、Go、Python、JAVA都是一样,任何语言都是一样,这才是最本源的东西,理解了fd关联的一系列结构,你才能对IO游刃有余。

简要的总结:

  1. 从姿势上来讲,用户open文件得到一个非负数句柄fd,之后针对该文件的IO操作都是基于这个fd;
  2. 文件描述符fd本质上来讲就是数组索引,fd等于5,那对应数组的第5个元素而已,该数组是进程打开的所有文件的数组,数组元素类型为struct file;
  3. 结构体task_struct对应一个抽象的进程,files_struct是这个进程管理该进程打开的文件数组管理器。fd则对应了这个数组的编号,每一个打开的文件用file结构体表示,内含当前偏移等信息;
  4. file结构体可以为进程间共享,属于系统级资源,同一个文件可能对应多个file结构体,file内部有个inode指针,指向文件系统的inode;
  5. inode是文件系统级别的概念,只由文件系统管理维护,不因进程改变(file是进程出发创建的,进程open同一个文件会导致多个file,指向同一个inode);

回顾一眼架构图:

内核把最复杂的活干了,只暴露给您最简单的一个非负整数fd。所以,绝大部分场景会用fd就行,倒不用想太多。当然如果能再深入看一眼知其所以然是最好不过。本文分享是基础准备篇,希望能给你带来不一样的IO视角。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号