[Linux系统] 一切皆文件 | 缓冲区机制 | FILE 结构

先说结论:

  1. 一切皆文件不是口号,而是内核把每个设备都装进 struct file,再用 file_operations 里的函数指针做回调,于是开发者拿着一个 fd 就能读文件、读系统状态、读管道
  2. 缓冲区是一块内存,让低速外设和高速 CPU 协调工作------它由 C 标准库提供,所以库函数有缓冲、write 没有,后面两个怪现象都源于此。

一、一切皆文件:Linux 的终极抽象

在 Windows 里是文件的,在 Linux 里也是文件;Windows 里不是文件的进程、磁盘、显示器、键盘,在 Linux 里同样被抽象成文件,可以用访问文件的方式取到信息。管道是文件,将来网络编程里的 socket,接口也和文件一致。

好处很直接:一套 API 通吃。

  • 几乎所有"读"都能用 read:读文件、读系统状态、读管道;
  • 几乎所有"改"都能用 write:改文件、改系统参数、写管道。

二、内核凭什么做到:struct file + file_operations

打开文件时,操作系统会为它创建一个 file 结构体(定义在 include/linux/fs.h),它同时属于所有设备:

c 复制代码
struct file {
    struct inode *f_inode;
    const struct file_operations *f_op;  // 函数指针表
    atomic_long_t f_count;   // 引用计数
    unsigned int  f_flags;   // 打开权限
    fmode_t       f_mode;    // 访问模式
    loff_t        f_pos;     // 当前读写位置
};

关键在于 f_op,它指向的 file_operations 里除了 owner 几乎全是函数指针:

c 复制代码
struct file_operations {
    loff_t (*llseek)(struct file *, loff_t, int);              // 改读写位置
    ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);
    int (*open)(struct inode *, struct file *);
    int (*release)(struct inode *, struct file *);
};

每个成员都对应一个系统调用。用户调 read,内核读出相应的函数指针并把控制权交出去,设备驱动的工作就完成了。每个设备都可以有自己的 read、write 实现,但通过这层回调,开发者只用 file 就能调取系统绝大部分资源------这就是"Linux 下一切皆文件"的核心理解。

本小节复盘和图示:

三、缓冲区:给高速 CPU 配一条缓冲带

缓冲区是内存空间的一部分:在内存中预留一块存储空间,用来缓冲输入或输出的数据。按对应设备方向,分为输入缓冲区和输出缓冲区。

为什么要引入它?不设缓冲区时,每次读写都要对磁盘发起系统调用,而一次系统调用意味着 CPU 从用户空间切到内核空间、发生上下文切换,CPU 时间是白花的,频繁的磁盘访问会严重拖慢程序。有了缓冲机制:从磁盘取信息时一次读入大量数据进缓冲区,后续访问不再走系统调用,取完再读下一批,磁盘读写次数大幅减少;打印机这类慢设备则先把文档送进缓冲区,自己慢慢打,CPU 转身去干别的事。

所以缓冲区的本质,就是一块用在设备和 CPU 之间、缓存数据的内存区,让低速外设与高速 CPU 协调工作。

3.1 三种缓冲类型

  • 全缓冲:填满整个缓冲区才做 I/O 系统调用,磁盘文件默认如此;
  • 行缓冲:遇换行符就刷新,终端设备默认如此,默认大小 1024;
  • 无缓冲:不缓存,直接调系统调用,stderr 就是如此,保证出错信息尽快显示。

除默认规则外,缓冲区满、执行 flush、进程结束也会触发刷新。

复盘和图示:

四、两个实验看穿缓冲行为

4.1 重定向后 printf 的内容去哪了

c 复制代码
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
printf("hello world: %d\n", fd);
close(fd);
// cat log.txt ------ 空的

明明把 1 号描述符重定向到了文件,为什么文件里什么都没有?因为 1 号指向磁盘文件后,刷新方式从行缓冲变成了全缓冲,内容没填满缓冲区自然不会落盘,加一句 fflush(stdout) 强制刷新即可。

另一个验证:把 2 号描述符重定向到文件,用 perror 输出:

c 复制代码
close(2);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
perror("hello world");   // 不用 fflush,直接写进文件

因为 stderr 没有缓冲区,内容立刻落盘,反向验证了"无缓冲"这一类型。

4.2 fork 之后为什么输出两遍

c 复制代码
printf("%s", "hello printf\n");
fwrite("hello fwrite\n", 13, 1, stdout);
write(1, "hello write\n", 12);
fork();

直接运行输出三行,但执行 ./hello > file 重定向到文件后出现五行------printf 和 fwrite 各输出两次,write 只有一次。

原因链条:

  • 重定向到普通文件后缓冲方式变为全缓冲,数据先留在库函数的用户级缓冲区
  • fork 时父子写时拷贝,各自持有一份未刷新的数据
  • 进程退出时统一刷新,两份都写进了文件
  • write 是系统调用,没有缓冲区,不受影响

顺带回答一个疑问:write 没有缓冲而 printf 有,说明该缓冲区是二次加上的,由 C 标准库提供;这里讨论的都是用户级缓冲区,OS 另有内核级缓冲区。

五、FILE 结构:fd 与缓冲区的载体

访问文件最终都要走 fd,所以 FILE 内部必然封装了 fd:

c 复制代码
typedef struct _IO_FILE FILE;      // stdio.h
struct _IO_FILE {
    int _flags;
    char *_IO_read_ptr, *_IO_read_end, *_IO_read_base;     // 读缓冲区指针
    char *_IO_write_base, *_IO_write_ptr, *_IO_write_end;  // 写缓冲区指针
    char *_IO_buf_base, *_IO_buf_end;                      // 缓冲区首尾
    int _fileno;   // 封装的文件描述符
};

一组指针描述读写缓冲区的起点与当前位置,_fileno 则是通往内核的钥匙。结论闭环:用户调 printf,数据先进 FILE 的缓冲区,满足刷新条件后再由 write 交给 fd,最后落到设备上。

六、总结

  • 一切皆文件:进程、磁盘、键盘、管道、socket 都被抽象成文件,read/write 一套 API 通吃;
  • 内核依据:f_op 指向 file_operations 函数指针表,把系统调用与设备驱动挂钩;
  • 缓冲区:内存中预留的一块空间,用空间换系统调用次数,让慢外设与快 CPU 协调;
  • 三种类型:全缓冲(磁盘文件)、行缓冲(终端,1024)、无缓冲(stderr),缓冲区满、flush、进程退出也会刷新;
  • 两张验方:重定向后 printf 不输出(全缓冲,需 fflush)、fork 后库函数输出两遍(写时拷贝带走未刷新数据),write 始终只一次。
相关推荐
blueSatchel1 小时前
UVC驱动源码研究
linux·嵌入式
高山有多高1 小时前
【Linux笔记】Linux基本指令
linux·运维·笔记
涉密IT资质笔记1 小时前
涉密人员脱密期管理规范:期限分级模型、就业限制边界与违规认定标准
java·服务器·前端·网络·数据库
honsor1 小时前
PoE温湿度传感器:一根网线供电+通信,即插即用,机房/配电室/仓库温湿度监测首选
运维·服务器·网络·数据库·人工智能·安全
自强的小白1 小时前
jvm面试(Gc)
服务器·jvm·面试
码上有光1 小时前
Linux:进程间通信——匿名管道通信、命名管道通信
android·java·linux·linux通信·匿名通信·命名通信
w01_02_031 小时前
cpu , 控制 ,计算。
linux
醇氧2 小时前
uvicorn 详细介绍
linux·运维·python·python3.11
Qwier2 小时前
Win Srv 2019 安装补丁后重启
运维·服务器·windows