1.什么是文件
文件是指内容+属性,所以文件永远不是只有内容;
1.1狭义上:
①文件的存储特性
永久性存储:磁盘是永久性存储介质,文件在磁盘上的存储是永久性的(断电不丢失)
②磁盘的设备属性
外部设备:磁盘属于外设(I/O设备)
双重角色:既是输出设备(写入数据),也是输入设备(读取数据)
③文件操作的本质
I/O操作:对磁盘上文件的所有操作,本质上都是对外设的输入和输出
统称IO:所有文件读写操作,都可以简称为IO
1.2广义上:
Linux 下⼀切皆⽂件(键盘、显⽰器、⽹卡、磁盘...... 这些都是抽象化的过程)

2.C语言文件IO复习
cpp
#include <stdio.h>
int main()
{
FILE * fp = fopen("text.log","w");
//文件不带路径默认就在当前路径下跑,
打开文件失败就会创建一个文件;
if(fp== NULL)
{
perror("fopen");
return 1;
}
//对文件进行操作
const char * message = "hello\n";
int i=0;
while(i<10)
{
fputs(message ,fp);
i++;
}
fclose(fp);
return 0;
}
fopen只有当运行到的时候才可以执行,你把代码写完了并没有执行,所以是我们的进程在打开,进程保存在内存中,CPU跑代码,但是文件在磁盘上,根据冯诺依曼属性,CPU没办法直接访问磁盘,所以文件也要加载到内存中,所以fopen本质上是把它加载到内存中;加载的是内容和属性,一个进程可以打开多个文件,呢么多个进程呢?文件就会变得多起来,呢操作系统当然也要管理加载到内存中的文件了,所以操作系统如何管理文件呢?"先描述,再组织"
在内核中,文件=文件的内核数据结构(struct fd_struct)+文件的内容,所以文件的属性也是会被加载到内存中;我们研究一个打开的文件,是在研究进程和文件关系;
呢么没有被打开的文件呢?还在磁盘上;打开的文件 在内存上;
管理内存上的文件还有磁盘上的文件这个概念叫做文件系统;



stdin是键盘文件中的一个FILE*结构体,stdout和stderr是显示器文件中的一个FILE*的结构体;
进程默认会打开这三个文件流;
我们所用的C文件接口,地城一定要分装对应的文件类的系统调用;
3.文件相关系统调用接口
3.1 打开文件的系统调用接口


只读、读写、只写、创建、追加;他们都是宏,int flags本质是一个32bit 位图,而我们的宏是只有一个比特位为1的值;
使用位图为我们的标记位,比之前的int flags方便很多;可以实现多功能组合代码;所以我们对open的标记位也可以组合;每一个进程都有一个umask,默认从系统来但是你自己设置了就是你的

返回值是一个整数,我们叫做文件描述符->fd
4.文件描述符
4.1什么是文件描述符
fd(文件描述符)
①本质:fd 是一个非负整数(int 类型),是操作系统内核为进程打开的文件 / IO 资源分配的唯一标识。
②内核会为每个进程维护一个「文件描述符表 」,fd 就是这个表的下标,通过下标可以找到对应的文件元信息(如文件指针、权限、文件类型等)。
③系统默认分配的fd:0(标准输入 stdin)、1(标准输出 stdout)、2(标准错误 stderr)
4.2 fd、FILE*、FILE之间的关系
① FILE(结构体)
本质:FILE 是 C 标准库(stdio.h)定义的一个结构体,是对底层 fd 的「封装和增强」。
它的核心作用是为底层 fd 增加缓冲机制(行缓冲 / 全缓冲 / 无缓冲),同时存储 fd 相关的元信息(如缓冲区指针、缓冲区大小、错误标志、文件位置指针等)。
cpp
typedef struct {
int fd; // 核心:关联的底层文件描述符
char* buf; // 缓冲区指针
int buf_size; // 缓冲区大小
int pos; // 缓冲区当前位置
int flags; // 文件状态标志(如读/写、缓冲类型)
int error; // 错误标志
} FILE;
②FILE*(FILE 结构体指针)
本质:FILE* 是指向 FILE 结构体的指针(因为 FILE 结构体的大小和细节对开发者透明,标准库要求必须通过指针操作)。
我们常用的 stdin、stdout、stderr 本质就是 FILE* 类型的全局指针,分别关联 fd 0、1、2。
常见操作:通过 fopen()、fread()、fwrite()、fclose() 等标准库函数操作 FILE*。

4.3文件描述符表和task_struct(PCB)的关系
task_struct:进程控制块(PCB),内核中描述一个进程的所有信息(PID、内存布局、文件描述符表等),每个进程有且仅有一个;
文件描述符表:属于 task_struct 的成员(files_struct 结构体),是进程私有的 ------每个进程都有独立的文件描述符表,因此不同进程的 fd 数值可以相同(比如都有 fd=1,但指向不同的文件表项);
用户空间的程序通过 fd(整数)访问时,内核会先找到当前进程的 task_struct → 取出文件描述符表 → 用 fd 作为索引找到对应的文件表项。

4.4 文件描述符的分配规则
在files_struct数组当中,找到当前没有被使用的最小的⼀个下标,作为新的文件描述符。
5.重定向
cpp
int main()
{
//测试重定向
close(1);
int fd=open("myfile",O_WRONLY|O_CREAT,0644);
if(fd<0)
{
perror("open");
return 1;
}
printf("fd= %d",fd);
fflush(stdout);
close(fd);
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中,fd=1。这
种现象叫做输出重定向。常见的重定向有: > , >> , <
那重定向的本质是什么呢?
文件重定向的本质,是修改进程对文件描述符(fd)的映射关系------ 把原本指向某个 IO 资源(如终端、标准输出)的 fd,重新指向另一个 IO 资源(如普通文件、管道、设备),让进程的 IO 操作 "无感" 地输出 / 输入到新目标。

但是我们发现代码中的fd还是等于1,这是理解文件描述符核心逻辑的关键问题 ------打印出 fd=1 恰恰印证了重定向的本质:复用 fd 编号,修改其指向的资源 。你打印的 fd=1 是文件描述符的编号 (数组索引),这个编号本身不会变;变的是这个编号在「文件描述符表」中指向的底层资源(从终端变成了文件)。
6.dup
dup(全称 duplicate)是 Linux 系统调用(<unistd.h>),核心作用是为同一个底层文件 / IO 资源,创建一个新的文件描述符(fd) ------ 两个 fd 指向内核中同一个 struct file 结构体,共享文件偏移量、打开模式等状态。可以通俗理解为:给同一个 "文件资源" 配两把不同编号的 "钥匙"(fd)。
7.理解一切皆文件
"一切皆文件" 的核心实现的是虚拟文件系统(VFS),它是 Linux 内核中一层关键的抽象软件层,并非真实的文件系统,核心作用是屏蔽底层不同硬件、不同文件系统(如 ext4、tmpfs)的差异,提供一套统一的接口。VFS 的核心抽象结构是struct file结构体,这个结构体是所有资源(普通文件、硬件设备、管道、socket 等)的统一 "身份标识",里面包含函数指针(如read、write,不同资源对应不同的底层实现)、文件偏移量、打开模式、权限标志、关联的索引节点(inode)等关键信息,是 VFS 实现 "多态" 的核心。
每个进程在内核中都有一张独立的文件描述符表,本质是一个存放struct file指针的数组,文件描述符(fd)就是这个数组的下标,用于快速索引对应的struct file。进程调用资源时,先通过用户态库函数(如 fopen)封装,触发系统调用(如 open)陷入内核态,内核通过 VFS 找到对应的struct file,再通过文件描述符表将 fd 与该结构体关联,进程后续只需操作 fd,就能间接操作底层资源。

上图中的外设,每个设备都可以有⾃⼰的read、write,但⼀定是对应着不同的操作⽅法!!但通过
struct file 下 file_operation 中的各种函数回调,让我们开发者只⽤file便可调取 Linux 系
统中绝⼤部分的资源!!这便是"linux下⼀切皆⽂件"的核⼼理解。
当操作硬件设备时,硬件会被抽象为设备文件(如键盘对应/dev/input/event0),其对应的struct file中,会存储设备的主从编号(标识硬件类型和具体设备)、设备驱动的操作函数指针(如读取键盘的read_keyboard、写入显示器的write_screen)、设备状态等信息,这些信息由内核和设备驱动填充,进程读写设备文件的 fd 时,VFS 会根据这些信息,将请求分发到对应的设备驱动,最终完成与硬件的交互。

8.内核级缓冲区
8.1什么是内核级缓冲区
读写⽂件时,如果不会开辟对⽂件操作的缓冲区,直接通过系统调⽤对磁盘进⾏操作(读、写等),那么每次对⽂件进⾏⼀次读写操作时,都需要使⽤读写系统调⽤来处理此操作,即需要执⾏⼀次系统调⽤,执⾏⼀次系统调⽤将涉及到CPU状态的切换,即从⽤⼾空间切换到内核空间,实现进程上下⽂的切换,这将损耗⼀定的CPU时间,频繁的磁盘访问对程序的执⾏效率造成很⼤的影响。
为了减少使⽤系统调⽤的次数,提⾼效率,我们就可以采⽤缓冲机制。⽐如我们从磁盘⾥取信息,可以在磁盘⽂件进⾏操作时,可以⼀次从⽂件中读出⼤量的数据到缓冲区中,以后对这部分的访问就不需要再使⽤系统调⽤了,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作⼤⼤快于对磁盘的操作,故应⽤缓冲区可⼤⼤提⾼计算机的运⾏速度。⼜⽐如,我们使⽤打印机打印⽂档,由于打印机的打印速度相对较慢,我们先把⽂档输出到打印机相应的缓冲区,打印机再⾃⾏逐步打印,这时我们的CPU可以处理别的事情。可以看出,缓冲区就是⼀块内存区,它⽤在输⼊输出设备和CPU之间,⽤来缓存数据。它使得低速的输⼊输出设备和⾼速的CPU能够协调⼯作,避免低速的输⼊输出设备占⽤CPU,解放出CPU,使其能够⾼效率⼯作。内核级缓冲区是 Linux 为了协调高速用户态 / 内存 与低速磁盘 I/O 而设计的核心缓存层,本质是一段内核态内存空间,用来暂存文件数据。它的存在主要有三个关键原因:
性能优化:磁盘 I/O 速度远慢于内存,直接读写磁盘会造成巨大性能损耗。缓冲区将多次小批量 I/O 合并为一次大块 I/O,减少磁盘访问次数,提升系统整体吞吐效率。
数据安全与原子性:修改文件时,内核先将数据写入缓冲区,而非直接覆盖磁盘原数据,避免中途断电或进程崩溃导致文件损坏,保证了数据修改的原子性与可靠性。
资源隔离与调度:缓冲区由操作系统自主管理刷盘时机,内核可根据负载、空闲时间等策略异步落盘,避免阻塞用户进程,同时实现多进程间的缓存共享,减少重复 I/O。
从流程上看,进程调用 write 时,数据先通过拷贝函数从用户态缓冲区(如 char buffer[])复制到内核缓冲区,此时进程即可继续执行后续逻辑,内核会在合适时机(如缓冲区满、主动调用 fsync)将数据刷入磁盘。修改文件的本质也是 "先读后写":内核先将磁盘数据读入缓冲区,修改后再写回,最终落盘。内核还通过 radix_tree 等结构高效管理缓冲区页,实现快速查找与缓存置换,进一步提升 I/O 效率。

8.2 缓冲区的种类
-
全缓冲区只有当整个缓冲区被填满时,才会执行实际 I/O 系统调用。磁盘文件通常默认使用全缓冲,以减少系统调用次数、提高读写效率。
-
行缓冲区 遇到换行符
\n时就会触发 I/O 系统调用;若缓冲区被填满,即使没有换行符也会立即刷新。终端相关的流(标准输入、标准输出)默认使用行缓冲,默认缓冲区大小为 1024 字节。 -
无缓冲区 标准 I/O 库不做缓存,数据直接调用系统调用。标准错误流
stderr通常使用无缓冲,保证错误信息能最快输出、不被延迟。
8.3什么时候缓冲区会刷新
①缓冲区满时
②调用fflush函数时
③进程结束时
9.虚拟文件系统
进程通过 task_struct 管理资源,其中 *files 指向 files_struct,其内部 file* fd_array[] 构成文件描述符表,文件描述符 fd 是该数组的下标,每个元素是指向 struct file 的指针。struct file 是内核对打开文件的抽象,包含操作表(函数指针集合)、文件偏移、权限等,同时关联文 件的内核级缓冲区,数据先从用户态拷贝到内核缓冲区,再由操作系统自主决定何时刷入磁盘(如 myfile),这一设计可减少磁盘 I/O 次数、提升性能。虚拟文件系统(VFS) 基于 struct file 实现统一抽象:普通文件、终端、设备等资源都被封装为 struct file,对外提供一致的 read/write 接口,进程调用 write(3, "hello", ...) 时,会通过 fd 找到对应 struct file,执行其 file->ops->write() 函数,完成数据从用户态到内核缓冲区的拷贝。修改文件的本质是先读取数据到缓冲区,修改后再写入,内核通过 radix_tree 结构高效管理缓冲区页,实现快速查找与缓存。
10.FILF
一般 C 库函数写入文件时是全缓冲的,而写入显示器是行缓冲。printf、fwrite 等库函数会自带用户级缓冲区(进度条的例子可佐证这一点):当输出重定向到普通文件时,数据的缓冲方式会从行缓冲变为全缓冲。存入缓冲区的数据不会被立即刷新,即便调用 fork 创建子进程后也是如此;但进程退出时,缓冲区会统一刷新,数据才会写入文件**。fork 时父子进程会发生数据的写时拷贝,父进程准备刷新缓冲区时,子进程也会拥有相同的一份缓冲区数据,最终导致文件中写入两份相同数据。write 系统调用无此现象,说明其本身不具备用户级缓冲区。**
综上:**printf、fwrite 等 C 库函数会自带用户级缓冲区,而 write 系统调用无用户级缓冲区。**此处讨论的缓冲区均为用户级缓冲区(内核级缓冲区虽由操作系统提供以提升整机性能,但不在本次讨论范围内)。该缓冲区的提供方:printf、fwrite 是 C 标准库函数,write 是系统调用;库函数处于系统调用的 "上层",是对系统调用的封装,而 write 无缓冲区、库函数有缓冲区,足以说明该用户级缓冲区是 C 标准库在封装系统调用时二次添加的。


