
1.什么是文件文件是指内容属性所以文件永远不是只有内容1.1狭义上①文件的存储特性永久性存储磁盘是永久性存储介质文件在磁盘上的存储是永久性的断电不丢失②磁盘的设备属性外部设备磁盘属于外设I/O设备双重角色既是输出设备写入数据也是输入设备读取数据③文件操作的本质I/O操作对磁盘上文件的所有操作本质上都是对外设的输入和输出统称IO所有文件读写操作都可以简称为IO1.2广义上Linux 下⼀切皆⽂件键盘、显⽰器、⽹卡、磁盘…… 这些都是抽象化的过程2.C语言文件IO复习#include stdio.h int main() { FILE * fp fopen(text.log,w); //文件不带路径默认就在当前路径下跑 打开文件失败就会创建一个文件 if(fp NULL) { perror(fopen); return 1 } //对文件进行操作 const char * message hello\n; int i0; while(i10) { 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,默认从系统来但是你自己设置了就是你的返回值是一个整数我们叫做文件描述符-fd4.文件描述符4.1什么是文件描述符fd文件描述符①本质fd是一个非负整数int 类型是操作系统内核为进程打开的文件 / IO 资源分配的唯一标识。②内核会为每个进程维护一个「文件描述符表」fd就是这个表的下标通过下标可以找到对应的文件元信息如文件指针、权限、文件类型等。③系统默认分配的fd0标准输入 stdin、1标准输出 stdout、2标准错误 stderr4.2 fd、FILE*、FILE之间的关系① FILE结构体本质FILE 是 C 标准库stdio.h定义的一个结构体是对底层 fd 的「封装和增强」。它的核心作用是为底层 fd 增加缓冲机制行缓冲 / 全缓冲 / 无缓冲同时存储 fd 相关的元信息如缓冲区指针、缓冲区大小、错误标志、文件位置指针等。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 数值可以相同比如都有 fd1但指向不同的文件表项用户空间的程序通过 fd整数访问时内核会先找到当前进程的 task_struct → 取出文件描述符表 → 用 fd 作为索引找到对应的文件表项。4.4 文件描述符的分配规则在files_struct数组当中找到当前没有被使用的最小的⼀个下标作为新的文件描述符。5.重定向int main() { //测试重定向 close(1); int fdopen(myfile,O_WRONLY|O_CREAT,0644); if(fd0) { perror(open); return 1; } printf(fd %d,fd); fflush(stdout); close(fd);此时我们发现本来应该输出到显示器上的内容输出到了文件 myfile 当中其中fd1。这种现象叫做输出重定向。常见的重定向有: , , 那重定向的本质是什么呢文件重定向的本质是修改进程对文件描述符fd的映射关系—— 把原本指向某个 IO 资源如终端、标准输出的 fd重新指向另一个 IO 资源如普通文件、管道、设备让进程的 IO 操作 “无感” 地输出 / 输入到新目标。但是我们发现代码中的fd还是等于1这是理解文件描述符核心逻辑的关键问题 ——打印出 fd1 恰恰印证了重定向的本质复用 fd 编号修改其指向的资源。你打印的fd1是文件描述符的编号数组索引这个编号本身不会变变的是这个编号在「文件描述符表」中指向的底层资源从终端变成了文件。6.dupdup全称 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 标准库在封装系统调用时二次添加的。