当前位置: 首页 > news >正文

Linux打开、读写一个文件内核做了啥?

目录

一、PCB中关于文件部分的结构体是什么?

二、files_struct的两个核心成员

三、fork函数原理

四、读写文件时的三层缓冲区


        我们在使用C语言打开一个文件,只需要简简单单的调用open函数,就会给我们返还一个FILE*的指针或者使用系统调用返还给我们fd,通过这个指针即可对文件进行读写,但是他的底层做了什么呢?内核又有什么数据字段来管理呢?

一、PCB中关于文件部分的结构体是什么?

        这是一段直接使用系统调用打开文件,并读写的示例:

#include <stdio.h>   // 标准输入输出
#include <fcntl.h>   // open函数需要
#include <unistd.h>  // read/write/close函数需要
#include <string.h>  // strlen函数需要int main() {// 1. 打开文件(如果不存在则创建)int fd = open("test.txt", O_RDWR | O_CREAT, 0644);if (fd == -1) {perror("打开文件失败");  // 打印错误原因return 1;}printf("文件打开成功,fd = %d\n", fd);  // 输出文件描述符// 2. 向文件写入数据const char *msg = "Hello, File!";ssize_t write_len = write(fd, msg, strlen(msg));if (write_len == -1) {perror("写入失败");close(fd);return 1;}printf("写入了 %zd 个字节\n", write_len);// 3. 移动文件指针到开头(否则读不到刚写入的内容)lseek(fd, 0, SEEK_SET);// 4. 从文件读取数据char buf[1024];ssize_t read_len = read(fd, buf, sizeof(buf)-1);  // 留1个字节存结束符if (read_len == -1) {perror("读取失败");close(fd);return 1;}buf[read_len] = '\0';  // 手动添加字符串结束符printf("读到的内容:%s\n", buf);// 5. 关闭文件close(fd);return 0;
}

fd一个非负整数(如上面的3),它是进程用来标识 “当前打开的文件” 的编号

fd由于也是进程的一个属性,所以他必然在PCB中。

struct task_struct 
{  // 进程的“档案袋”...struct files_struct *files;  // 管理所有打开的文件...
};struct files_struct 
{atomic_t count;struct fdtable __rcu *fdt;struct fdtable fdtab;spinlock_t file_lock ____cacheline_aligned_in_smp;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;
};struct fdtable 
{  // “文件索引表”(核心!)struct file *fd[1024];  // 数组:索引=fd值,内容=文件的详细信息// 例如:fd[0] → 标准输入;fd[1] → 标准输出;fd[3] → 我们打开的test.txt
};

        在这幅图中,我们首先要明白PCB和files_struct的从属关系,还是和之前一样,进程的所有信息都被存放在PCB中。当你调用open创建或者打开一个文件的时候,内核本质上是在你进程的PCB中创建了一个files对象,替换原本的NULL指针。

        然后对于files_struct我们看看其中关键的几个成员(如图所示):

(1)atomic_t count:

   这是一个原子计数器,用于记录当前有多少个引用指向这个 files_struct 结构体。在 Linux 内核中,当发生进程复制(如 fork 操作)时,不会立马给子进程创建files_struct结构体。(因为如果子进程创建后直接exec了,那么给子进程创建结构体就是一个资源浪费了)而是子进程会共享父进程的 files_struct 结构体,此时计数器的值会增加。只有当子进程或父进程打开或者关闭某个文件描述符的“写”操作,才会给子进程创建一个新的,复制瞬间两者完全一样。当一个进程关闭所有文件并销毁其 files_struct 结构体时,计数器的值会减少,当计数器变为 0 时,内核就可以释放该 files_struct 结构体所占用的资源。

(2)struct fdtable __rcu *fdt:

   这是一个指向 fdtable 结构体的指针,并且使用了 __rcu(Read - Copy - Update)机制。fdtable 结构体用于管理文件描述符表,fdt 指针指向当前正在使用的文件描述符表。__rcu 机制允许在不阻塞读操作的情况下对 fdt 进行更新,提高了并发访问时的性能。

(3)struct fdtable fdtab:

   这是一个 fdtable 结构体实例,作为备用的文件描述符表。在一些情况下,例如当需要动态扩展文件描述符表时,内核会先操作这个备用表,然后再切换到新的表,以保证操作的原子性和正确性。

(4)spinlock_t file_lock ____cacheline_aligned_in_smp:

         这是一个自旋锁,用于保护对 files_struct 结构体中共享资源的访问。在多处理器系统(SMP)中,当多个内核线程可能同时访问和修改 files_struct 中的数据(比如添加或删除文件描述符 )时,通过自旋锁可以避免竞态条件,保证数据的一致性和正确性。____cacheline_aligned_in_smp 用于确保自旋锁在 SMP 系统中以缓存行对齐的方式存储,减少缓存冲突,提高性能。

(5)int next_fd:

   记录下一个可用的文件描述符的值。当进程打开一个新文件时,内核会从 next_fd 开始查找空闲的文件描述符,并将其分配给新打开的文件,然后更新 next_fd 的值。

(6)unsigned long close_on_exec_init[1]:

   这是一个数组,用于记录在执行 exec 系列系统调用(如 execve )时,哪些文件描述符应该被关闭。每个比特位对应一个文件描述符,比特位为 1 表示该文件描述符在 exec 时应该被关闭。初始状态下,这个数组的值会被初始化,用户可以通过 fcntl 系统调用的 FD_CLOEXEC 标志来修改特定文件描述符对应的比特位。初始情况下可以看到他只给了1个long类型的位图,即64个文件描述符,一般情况下是够用的,如果超过了内核会有一个close_on_exec的新位图来接替他的工作。

(7)unsigned long open_fds_init[1]:

   也是一个数组,用于记录当前已经打开的文件描述符。每个比特位对应一个文件描述符,比特位为 1 表示该文件描述符已经被打开。

(8)unsigned long full_fds_bits_init[1]:

   这个数组用于表示文件描述符表的哪些位置被占用了。当文件描述符表中的位置被占用时,对应的比特位会被设置。即方便了next_fd的查询。

(9)struct file __rcu ** fd_array:

   这是一个指向 struct file 指针数组的指针。fd_array 数组用于存储文件描述符和对应的 struct file 结构体之间的映射关系,数组的索引就是文件描述符的值,数组元素是指向对应文件的 struct file 结构体的指针。而file这个结构体负责描述打开文件的状态,如文件偏移量、文件操作方法、文件读写权限、和引用计数。

二、files_struct的两个核心成员

        他们分别是文件描述符表,和file结构体的fd_array二级指针。

        而文件描述符表的核心部分是一个file结构体的指针数组,通过下标可以找到文件描述符对应的file结构体,从而获取file的更多信息。

我们以read为例:

  1. 用户态调用 read(fd, buf, n),传入文件描述符 fd
  2. 内核通过当前进程的 task_struct找到 files_struct
  3. 从 files_struct->fdt 拿到当前活跃的文件描述符表,通过 fd 作为索引访问 fd_array[fd],得到指向 struct file 的指针
  4. 查看 struct file 中的关键信息:
    • f_pos:确定当前读写位置(比如上次读到了文件的第 100 字节,这次从 100 开始读)
    • f_op:获取文件操作方法集(比如 f_op->read 指向具体的读函数,不同文件系统 / 设备的读逻辑不同,都通过这里抽象)
    • f_path:找到文件在文件系统中的位置(比如对应哪个 inode,数据存在磁盘的哪个块)
  5. 执行实际的读操作(从磁盘 / 缓存读取数据),并更新 f_pos(比如读了 50 字节,f_pos 变为 150)
  6. 将数据返回给用户态的 buf

当然,如果一个进程多次打开同一个文件,会有多个fd被记录。

三、fork函数原理

        当使用fork创建一个进程的时候,首先会给子进程分配一个PCB。这是进程在内核中的核心描述符,记录了进程的所有关键信息,如进程状态、调度信息、内存管理信息等。

   task_struct 中的很多字段会直接从父进程复制过来(写时拷贝计数),不过也有一些字段需要进行特殊处理,例如子进程的 PID 是唯一的,需要内核进行分配。

        Linux 采用写时复制(Copy - On - Write,COW)技术来处理父子进程的内存。写时复制的核心思想是,在 fork 发生时,子进程并不立即复制父进程的所有内存页面,而是与父进程共享这些页面。父子进程的页表会指向相同的物理内存页,并将这些页标记为只读。只有当父子进程中的某一个试图修改共享的内存页面时,内核才会为修改的页面分配新的物理内存,并更新相应的页表,使得父子进程各自拥有独立的内存副本。

  • 共享模式:在默认情况下,子进程会共享父进程的 files_struct 结构体。这意味着父子进程共享相同的文件描述符表,也就是它们共享相同的 fd_array。例如,若父进程打开了一个文件,文件描述符为 fd,那么子进程也可以通过这个 fd 访问同一个文件。此时,files_struct 中的 atomic_t count 原子计数器的值会增加,以记录有多少个进程(这里是父子两个进程)共享这个 files_struct 结构体。
  • 分离情况:当父子进程中的某一个对文件描述符进行修改操作(如 close 一个文件描述符,或者通过 fcntl 更改文件描述符的属性)时,内核会为子进程创建一个新的 files_struct 结构体副本。这个副本会复制父进程 files_struct 中的大部分信息,包括文件描述符表(fdtable)的状态,但它们从此各自独立。这是因为写时复制技术不仅应用于内存,也适用于像 files_struct 这样的资源管理结构,目的是减少不必要的资源复制开销,提高系统性能。

        当父子进程任意一个对自己打开的文件进行了修改,如修改文件权限、打开、关闭文件。内核会剥离二者的files_struct,各自独享一份,在复制瞬间二者是完全一样的。

四、读写文件时的三层缓冲区

我们先来看看C语言中FILE结构体是如何对fd进行封装的:

// 简化自glibc的stdio.h实现
typedef struct _IO_FILE FILE;struct _IO_FILE {int _flags;           // 文件状态标志(如读写模式、缓冲模式等)
#define _IO_FILE_FLAGS 0x00000fff// 缓冲区相关char* _IO_buf_base;   // 缓冲区起始地址char* _IO_buf_end;    // 缓冲区结束地址(缓冲区大小 = _IO_buf_end - _IO_buf_base)char* _IO_ptr_base;   // 当前缓冲区中数据的起始位置char* _IO_ptr_end;    // 当前缓冲区中数据的结束位置(下一个写入位置)// 文件描述符int _fileno;          // 封装的文件描述符(fd),关联内核的文件操作// 错误和EOF标志int _IO_errno;        // 错误码(类似errno)int _IO_eof;          // EOF标志(1表示已到达文件末尾)// 缓冲模式控制enum {_IO_UNBUFFERED,   // 无缓冲(如stderr)_IO_LINE_BUF,     // 行缓冲(如stdout,遇到换行符刷新)_IO_FULL_BUF      // 全缓冲(默认模式,缓冲区满时刷新)} _IO_buf_mode;// 其他辅助成员(简化省略)struct _IO_FILE* _chain;  // 用于链接多个FILE结构(如stdio列表)off_t _offset;            // 当前文件位置(部分情况下使用)
};

从这张图我们可以知道:

(1)每一个FILE结构体内部都有一个缓冲区,所以各个文件数据不会互相干扰。

(2)当想通过C语言向磁盘文件中写入数据的时候,会首先进入C语言层面的缓冲区、然后进入内核缓冲区,最后才能修改磁盘文件的内容。即经过了2次缓存。

(3)我们在使用C语言的时候,一般还会自己定义一个用于存放数据的缓冲区,比如数组、字符串然后才是将这些内容写入到FILE中的缓冲区,所以大多数实际使用经过了3次缓存。

http://www.dtcms.com/a/293100.html

相关文章:

  • python安装package和pycharm更改环境变量
  • MySQL:内置函数
  • 基于模拟的流程为灵巧机器人定制训练数据
  • 钢铁逆行者:Deepoc具身智能如何重塑消防机器人的“火场直觉”
  • CY3-NH2/amine 使用注意事项
  • 【nginx】隐藏服务器指纹:Nginx隐藏版本号配置修改与重启全攻略
  • Adaptive Graph Convolutional Network for Knowledge Graph Entity Alignment
  • 基于LangGraph的Open Deep Research架构全解析:从多Agent协作到企业级落地
  • 数据库设计mysql篇
  • 什么是检索增强生成(RAG)?
  • java调用周立功USBCAN SDK读取汽车总线数据
  • [3-02-02].第04节:开发应用 - RequestMapping注解的属性2
  • TCP头部
  • Kotlin伴生对象
  • Go后端配置文件教程
  • LeetCode|Day22|231. 2 的幂|Python刷题笔记
  • AI一周事件(2025年7月15日-7月21日)
  • 开发避坑短篇(4):跨域请求中Session数据丢失的排查与修复方案
  • Qt资源系统:如何有效管理图片和文件
  • 【黑马SpringCloud微服务开发与实战】(五)微服务保护
  • 【NLP舆情分析】基于python微博舆情分析可视化系统(flask+pandas+echarts) 视频教程 - 访问鉴权功能实现
  • MMDeploy模型转换与TensorRT推理遇到问题及解决方案
  • GRU模型
  • Trae安装指定版本的插件
  • Clickhouse源码分析-副本数据同步
  • 网络编程---TCP协议
  • Spring AI 系列之十九 - Ollama集成Deepseek
  • 配置https ssl证书生成
  • 数字护网:一次深刻的企业安全体系灵魂演练
  • 接口测试用例选择及效能优化策略