Linux中基础IO知识全解

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 等)的统一 "身份标识",里面包含函数指针(如readwrite,不同资源对应不同的底层实现)、文件偏移量、打开模式、权限标志、关联的索引节点(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 标准库在封装系统调用时二次添加的。

相关推荐
等风来不如迎风去2 小时前
【linux】tar [选项] 归档文件名 要打包的文件/目录..
linux·运维·elasticsearch
编程之升级打怪2 小时前
简单的测试搜索词的分割算法思路
java·算法
.select.2 小时前
虚函数和虚表
开发语言·c++·算法
靠沿2 小时前
【优选算法】专题十七——多源BFS(最短路径问题)
java·算法·宽度优先
重生之我是Java开发战士2 小时前
【递归、搜索与回溯】优美的排列,N皇后,有效的数独,解数独,单词搜索,黄金矿工,不同路径III
算法·深度优先
yuuki2332332 小时前
【Linux】Linux基本指令 & 权限全解析
java·linux·服务器
ejjdhdjdjdjdjjsl2 小时前
halcon算子
人工智能·算法·计算机视觉
Aawy1202 小时前
C++与Rust交互编程
开发语言·c++·算法