一、理解"一切皆是文件"
已知:
-
虽然每个硬件的驱动程序都不一样,但是一定会有对应的 read write 方法,实现方式不一样的,但是一定有类似的方法
-
os 不会跟硬件打交道,但是 os 需要保证 对于硬件设备,要先描述在组织的管理起来,因此这些硬件设备要被 os 知道,所以一定会有对应的数据结构,有对应的属性和方法
那么大体的轮廓已经有了,现在的问题就是:
这个 struct file 是怎么访问 硬件的呢?
也就是说个结构体,里面没有方法
但是可以证明 函数指针!!!
此时就可以通过 函数指针,去查找对应的方法,进而进行硬件的访问
那么数据在哪? 在 os 的文件缓冲区里面。
这样子逻辑就串起来了。
因此总体的流程是:
- 通过文件描述符,访问特定的文件,
- 例如,可以通过 write 系统调用 把用户的文件缓冲区拷贝到 os 内核的文件缓冲区,
- 然后可以通过 函数指针,访问硬件驱动的函数,把 os 内核的文件缓冲区数据 交给硬件
为什么这样子设计呢?
是为了屏蔽底层硬件的差异
理解一切皆是文件的切入点:
在系统当中,访问任何设备,只要提供文件描述符,就不需要关注底层设备的差异,
而是通过 struct file 中的函数指针,就可以对文件进行访问

首先,在windows中是文件的东西,它们在linux中也是文件;其次一些在windows中不是文件的东西,比如进程、磁盘、显示器、键盘这样硬件设备也被抽象成了文件,你可以使用访问文件的方法访问它们获得信息;甚至管道,也是文件;将来我们要学习网络编程中的socket(套接字)这样的东西,使用的接口跟文件接口也是一致的。
这样做最明显的好处是,开发者仅需要使用一套API和开发工具,即可调取Linux系统中绝大部分的资源。举个简单的例子,Linux中几乎所有读(读文件,读系统状态,读PIPE)的操作都可以用read函数来进行;几乎所有更改(更改文件,更改系统参数,写PIPE)的操作都可以用write函数来进行。
之前我们讲过,当打开一个文件时,操作系统为了管理所打开的文件,都会为这个文件创建一个file结构体,该结构体定义在 /usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/fs.h 下,以下展示了该结构部分我们关系的内容:


我们看一下 struct file_operations 里面的内容:

里面就包含了各种操作的函数指针!
file_operation 就是把系统调用和驱动程序关联起来的关键数据结构,这个结构的每一个成员都对应着一个系统调用。读取 file_operation 中相应的函数指针,接着把控制权转交给函数,从而完成了 Linux 设备驱动程序的工作。
介绍完相关代码,一张图总结:
二、缓冲区
2.1. 什么是缓冲区
缓冲区是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。
其中:
- os 管理内存,会以4kb大小分为各种内存块
- 内存4 GB是以4GB除以4kb个 数据页 构成的
- 操作系统中存在很多很多的内存块,
- 有的是被进程申请的,有的没有被占用,有的是正常使用,有的是要被清理的,
- os 要管理这些内存块,先描述,在组织,叫做 struct_page
- 一个内存块找到对应的 struct_page,就能找到内存块
- 所以一个进程的文件缓冲区重点维护进程的 struct_file 以及 struct_page 之间的关系
2.2. close(fd) 的添加带来了什么?
cpp
int main()
{
close(1);
int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);
printf("fd:%d\n", fd);
printf("hello world\n");
printf("hello world\n");
// 不写close
return 0;
}

此时写入成功,如果加入了 close 语句,看看情况:
cpp
int main()
{
close(1);
int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);
printf("fd:%d\n", fd);
printf("hello world\n");
printf("hello world\n");
// 不写close
close(fd);
return 0;
}

此时我们发现,没有写入成功。
上面代码的关键分歧点:close(fd) 的添加带来了什么?
| 情况 | 代码操作 | 底层发生了什么 | 最终结果 |
|---|---|---|---|
| 不写close(fd) | 程序结束(return 0) | C标准库在程序退出时,会自动刷新(fflush)stdout 的用户级缓冲区,将数据通过系统调用 write 写入内核,最终落到磁盘文件。 | 写入成功 ✅ |
| 添加close(fd) | 显式关闭文件描述符 | 注意: close 是系统调用 ,它直接通知内核释放文件描述符 1。但此时 printf 写入的"hello world"还在 C 标准库的用户级缓冲区里(内存中),并没有被 wirte 刷进内核。一旦你提前 close了底层的 fd,缓冲区后续想刷的时候发现 fd 已经无效了(EBADF),数据就直接丢失了。 |
写入失败(数据丢失) ❌ |
我们在 close 后添加 系统调用 write ,看一下情况:
cpp
using namespace std;
int main()
{
close(1);
int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);
printf("fd:%d\n", fd);
printf("hello world\n");
printf("hello world\n");
const char *msg = "hello msg\n";
write(fd, msg, strlen(msg));
close(fd);
return 0;
}

此时,我们发现 ,使用系统调用的 write,就写入到 log.txt 文件中,这个是为什么?
2.3. write为什么能"救回"数据?
关键区别在于数据存放的位置 和刷新的主动权:
| 函数 | 数据存放位置 | 刷新由谁控制 | 受 close(fd) 影响吗? |
|---|---|---|---|
| printf | C语言用户级缓冲区(进程内存中的 FILE 结构体) | 由C库决定(缓冲区满、fflush、return 0 时触发 write 系统调用) | 极大影响。close 先关掉了内核中的 fd,等C库想刷数据时,系统调用 write 找不到有效的 fd,直接报错 EBADF,数据丢失。 |
| write | 操作系统内核级缓冲区(文件页缓存 page cache) | 由操作系统内核管理(异步刷盘) | 不受影响 。write 是系统调用,它直接 把用户态的数据拷贝进内核缓冲区。只要 write 执行在 close 之前,数据已经交给内核了。之后你调用 close 只是释放文件描述符表项,内核已经持有的数据会继续正常落 |
我们画图来解释一下:

printf 是把货堆在"菜鸟驿站"(用户态缓冲区),等着快递员(C库刷新机制)来取;而 write 是你自己开着卡车,直接把货送进了"中转站"(内核缓冲区)。
调用 close 相当于把快递员进站的门给关了(关闭 fd),所以驿站里的货(printf数据)永远寄不出去;但你自己开车送的货(write数据)已经在中转站里了,门关不关都不影响它被发往目的地(磁盘)。
2.4. 问题
问题1:上面的代码怎么进行更改
在 close(fd) 之前,加入 新函数:
cpp
#include <stdio.h>
int fflush(FILE *stream);
fflush会强制将 C 库用户态缓冲区(FILE 结构体内部)里积压的所有数据,通过系统调用 write 立即"搬运"到操作系统内核的缓冲区中。 执行完 fflush后,该用户态缓冲区会被清空。
cpp
#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;
int main()
{
close(1);
int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);
printf("fd:%d\n", fd);
printf("hello world\n");
printf("hello world\n");
fflush(stdout);
const char *msg = "hello msg\n";
write(fd, msg, strlen(msg));
close(fd);
return 0;
}

问题2:什么时候要刷新
-
强制刷新 如 fflush
-
刷新条件满足
-
进程退出
满足其一就行
刷新方式分为三类:如 write
-
立即刷新 --- 无缓冲 --- 写透模式
-
满了 --- 全缓冲 --- 效率最高 --- 普通文件一般采用这种方式
-
行刷新 --- 行缓冲 --- 显示器用
注意:这些都是语言层面的刷新方式
在 os 内核的缓冲区的刷新方式会更加复杂,不做说明。
解决上一个博客中的一个问题:
cpp
int main()
{
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
if (fd < 0)
exit(1);
dup2(fd, 1);
// 默认向显示器输出
printf("fd: %d\n", fd);
printf("hello file\n");
printf("hello file\n");
printf("hello file\n");
fprintf(stdout, "hello stdout\n");
fprintf(stdout, "hello stdout\n");
fprintf(stdout, "hello stdout\n");
const char *msg = "hello world\n";
write(fd, msg, strlen(msg));
return 0;
}

为什么第一行是 hello world?
这是因为 是不同的 刷新策略导致的!
stdout 被重定向到了普通文件,缓冲模式发生了改变。
-
默认情况(显示器) :stdout 是行缓冲,遇到 \n 就会立即刷新(调用 write)。
-
重定向到普通文件后 :C 库检测到 1 号文件描述符不再指向终端(isatty(1) 为假),会自动将 stdout 的缓冲模式切换为全缓冲。
在全缓冲模式下,即便有 \n,也不会触发系统调用 write。数据会一直攒在用户态缓冲区里,直到:
-
缓冲区满了(默认 4KB 或 8KB);
-
程序正常退出(return 0);
-
手动调用 fflush。
因此,所有的 printf / fprintf 都老老实实地待在"菜鸟驿站(用户态缓冲区)"里等待,根本没过"收费站(内核)"。
为什么 write 能插队并占据第一行?
因为 write(fd, msg, strlen(msg)) 是系统调用 ,它根本不走 C 语言的 FILE 缓冲区。
-
它是直接带着数据去敲内核的门。
-
此时内核中 log.txt 的文件偏移量还是 0(因为之前没有任何数据进入内核)。
-
所以内核二话不说,把 hello world\n 写在了文件的第一行,然后把偏移量移动到 12。
| 时间顺序 | 谁在操作 | 数据去了哪里 | 文件偏移量位置 | 最终文件里的行序 |
|---|---|---|---|---|
| 第 1 步 | printf / fprintf | 进 用户态缓冲区(积压) | 内核偏移量:0 | 还在内存里,未落盘 |
| 第 2 步 | write 系统调用 | 直接进内核(page cache) | 从 0 写入,后移到 12 | 占据第 1 行 ✅ |
| 第 3 步 | return 0 触发刷新 | 用户态缓冲区数据进内核 | 从 12 往后写入 | 被挤到了第 2、3... 行 |
一句话总结
printf 的执行顺序在前,不等于写入文件的顺序在前。 因为 printf 只是把货放在了"用户态缓冲区"这个仓库里,而 write 是直接开着卡车冲进内核抢占了 0 号车位。等 printf 的货(return 0 时)慢悠悠运过来时,只能停在 hello world 后面的车位上了。
问题3:为什么要设计两个缓冲区?
系统调用是有成本的,节约成本
问题4:这个语言层面的缓冲区到底在哪?
在 FILE 结构体里面
我们在使用库函数的 printf 或者 fprintf snprintf 这些函数中,要向显示器 stdout 打印刷新的时候,传递的是 stdout, 而 stdout 是 FILE* 类型的。
问题5:刷新的本质是什么
拷贝,数据交给系统,交给硬件,都是拷贝
计算机数据流动的本质:一切皆是拷贝
怎么理解格式化输出
本质就是把 %d %s 这些格式化输出成字符串,
然后放到c语言提供的缓冲区里面
2.5. 用户态缓冲区、系统调用、进程复制(fork) 三者之间的交互关系
cpp
#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;
int main()
{
// 库函数
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
const char *s = "hello fwrite\n";
fwrite(s, strlen(s), 1, stdout);
// 系统调用
const char *ss = "hello write\n";
write(1, ss, strlen(ss));
return 0;
}

正常输出没有问题。
cpp
#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;
int main()
{
// 库函数
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
const char *s = "hello fwrite\n";
fwrite(s, strlen(s), 1, stdout);
// 系统调用
const char *ss = "hello write\n";
write(1, ss, strlen(ss));
fork();
return 0;
}

为什么此时打印出来的内容,有些是出现了一次,有些出现了好多次。
已知内容:
| 函数类型 | 函数举例 | 数据路径 | 是否经过用户态缓冲区 |
|---|---|---|---|
| 库函数(带缓冲) | printf, fprintf, fwrite | 数据先进入 stdout 的 FILE 结构体内部的用户态缓冲区 | 是 |
| 系统调用(无缓冲) | write | 数据直接越过 C 库,通过内核提供的接口进入内核态 | 否 |
阶段 1:程序执行库函数(数据堆积在用户态)
cpp
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
fwrite(s, strlen(s), 1, stdout);
-
这三行代码执行完后,数据并没有被系统调用 write 送入内核。
-
它们只是被拷贝到了 stdout 所对应的用户态 FILE 结构体的缓冲区里。
-
关键点:此时进程的用户态内存中,已经存放了这三行字符串数据。
阶段 2:执行系统调用(直接闯入内核)
cpp
write(1, ss, strlen(ss));
-
这行代码是系统调用,它不经过用户态缓冲区,直接把 "hello write\n "交给了内核。
-
内核收到数据后,立即将其写入文件(或显示到终端)。
-
关键点:write 的数据已经"发车"了,跟后续的 fork 没有任何关系。
阶段 3:调用 fork()(复制进程内存空间)
cpp
fork();
-
fork() 系统调用会创建一个子进程,这个子进程是父进程的完整副本。
-
复制的内容 :包括代码、数据、堆栈,以及......用户态缓冲区里的内容!
-
此时,子进程的用户态缓冲区里,也完整复制了那三行库函数的数据("hello printf\n" 等)。
-
父进程的用户态缓冲区里,依然保留着同样的三行数据。
最后:父子进程退出(各自刷新缓冲区)
-
父进程退出:C 运行时环境会刷新 stdout 缓冲区,将里面的三行数据写入内核(输出 1 次)。
-
子进程退出:C 运行时环境同样会刷新自己的 stdout 缓冲区,将里面复制的三行数据也写入内核(再输出 1 次)。
结果 :库函数的三行数据,被输出了 2 次。
执行流程:
| 时间线 | 父进程行为 | 子进程行为 | 内核/文件状态 |
|---|---|---|---|
| 1. 执行库函数 | 三行数据写入用户态缓冲区 | (尚未创建) | 无数据 |
| 2. 执行 write | "hello world\n"直接进入内核 | (尚未创建) | 写入 1 次 |
| 3. 执行 fork() | 缓冲区有数据(三行) | 复制父进程缓冲区(三行) | 无变化 |
| 4. 父进程退出 | 刷新缓冲区 → 输出三行 | (等待退出) | 输出第 1 次 |
| 5. 子进程退出 | (已退出) | 刷新缓冲区 → 输出三行 | 输出第 2 次 |
三、模拟实现 FILE
cpp
#pragma once
#include <cstdio>
#define MAX 1024
#define NONE_FLUSH (1 << 0)
#define FULL_FLUSH (1 << 1)
#define LINE_FLUSH (1 << 2)
typedef struct IO_FILE
{
int _fileno;
int _flag;
char _outbuffer[MAX];
int _bufferlen;
int _flush_method;
} MyFile;
MyFile *MyFopen(const char *path, const char *mode);
void MyFclose(MyFile *file);
void Myfflush(MyFile *file);
int MyWrite(MyFile *file, void *str, int len);
cpp
#include "mystdio.h"
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define MODE 0666
MyFile *CreateFile(int fd, int flag)
{
MyFile *file = (MyFile *)malloc(sizeof(MyFile));
if (NULL == file)
{
perror("Create File error");
exit(1);
}
file->_fileno = fd;
file->_flag = flag;
file->_bufferlen = 0;
file->_flush_method = LINE_FLUSH;
memset(file->_outbuffer, 0, sizeof(file->_outbuffer));
return file;
}
MyFile *MyFopen(const char *path, const char *mode)
{
int flag = 0, fd = -1;
// 1. 判断是什么类型的
if (strcmp(mode, "w") == 0)
{
flag = O_CREAT | O_TRUNC | O_WRONLY;
fd = open(path, flag, MODE);
}
else if (strcmp(mode, "a") == 0)
{
flag = O_CREAT | O_APPEND | O_WRONLY;
fd = open(path, flag, MODE);
}
else if (strcmp(mode, "r") == 0)
{
flag = O_RDONLY;
fd = open(path, flag);
}
if (fd < 0)
{
perror("MyFopen failed!");
exit(1);
}
return CreateFile(fd, flag);
}
void MyFclose(MyFile *file)
{
if (file->_fileno < 0)
return;
Myfflush(file);
close(file->_fileno);
file = NULL;
}
void Myfflush(MyFile *file)
{
// 把用户的缓冲区拷贝到系统中
int n = write(file->_fileno, file->_outbuffer, file->_bufferlen);
// 把系统的缓冲区拷贝到磁盘外设上
fsync(file->_fileno);
file->_bufferlen = 0;
}
int MyWrite(MyFile *file, void *str, int len)
{
// 1. 把文件里面的缓冲区的内容 拷贝到 str
memcpy(file->_outbuffer + file->_bufferlen, str, len);
file->_bufferlen += len;
// 2. 判断刷新条件
if ((file->_flush_method & LINE_FLUSH) && file->_outbuffer[file->_bufferlen - 1] == '\n')
{
Myfflush(file);
}
return 0;
}
cpp
#include "mystdio.h"
#include <string.h>
#include <unistd.h>
int main()
{
MyFile *fp = MyFopen("./log.txt", "a");
char *msg = (char *)"hello myFile!!!";
int cnt = 3;
while (cnt--)
{
MyWrite(fp, msg, strlen(msg));
printf("buffer:%s\n", fp->_outbuffer);
sleep(1);
}
MyFclose(fp);
return 0;
}
