024 基础 IO —— 缓冲区

024 基础 IO —— 缓冲区
小米里的大麦缓冲区
1. 为什么需要缓冲区?(核心原因)
先出结论:格式化 ➔ 拼接成大数据块 ➔ 缓冲 ➔ 统一输出 ➔ 保证数据连贯,减少系统调用,提高效率!
1. 提高 I/O 效率!
硬件设备(尤其是磁盘、网络、终端)操作 很慢很慢,每次读写都直接操作设备 ➔ 太耗时 ➔ 系统负担重。
所以:
- 少量多次 ➔ 聚集成大量一次。
- 把「很多小的写操作」放到内存中,攒到一定量再「统一批量」写入磁盘/屏幕。
- 减少系统调用(syscall)次数 ➔ 提高整体程序运行效率。
简单比喻:你买菜如果每买一根葱就跑一次超市,累不累?当然要一次性买一堆,装个购物袋带回来!(这就是缓冲思想)
2. 配合格式化
printf("名字:%s, 年龄:%d\n", name, age);
这种格式化输出,本质上做了两件事:
- 格式化处理(Format)
%s
、%d
等占位符 ➔ 根据数据类型,把name
和age
这两个变量 格式化成字符串。- 例如:把整数
23
转成"23"
,字符串"Tom"
保持原样。
- 统一输出(Buffer)
- 把格式化好的整个大字符串(比如
"名字:Tom, 年龄:23\n"
)统一写入 缓冲区 - 最后一次性刷出去(flush 到终端/文件)
- 把格式化好的整个大字符串(比如
如果没有缓冲区,结果会很糟糕:
- 每遇到一个小块格式化的数据就立刻
write()
系统调用 ➔ 系统调用开销非常大(context switch)! - 输出内容 碎片化 ➔ 终端打印时出现撕裂、乱码、数据乱序。
- 多线程程序中可能出现 打印穿插错乱。
所以:必须先格式化 → 再统一放入缓冲区 → 再统一刷新输出!这样可以:
- 保证输出内容的原子性(一整块输出,保持顺序一致性)
- 提高I/O效率(少系统调用)
- 减少设备压力
2. C 语言的缓冲区(库缓冲区)
在 C 语言中,当使用 printf
、fprintf
、scanf
、fread
等 标准 I/O 函数 时,默认是有自己的 「用户态缓冲区」 的!这些缓冲区存在于内存(堆/栈)里,由 glibc
(标准 C 库)管理。注意:printf()
输出不一定立刻 write()
,是因为它先写入 C 标准库的缓冲区。
3. 缓冲区分类(死记!)
类型 | 触发条件 | 典型应用 |
---|---|---|
无缓冲(unbuffered) | 直接写入设备,不缓存 | stderr 、低层 read /write |
行缓冲(line buffered) | 遇到换行符 \n 或者缓冲区满就刷新 |
stdout (连接终端时) |
全缓冲(fully buffered) | 缓冲区满才刷新 | 文件流 (比如 fopen ) |
具体解释:
1. 无缓冲(Unbuffered)
- 直接
write
到设备。例如:stderr
(标准错误)。 - 因为错误信息要 第一时间输出,不允许缓存延迟!
2. 行缓冲(Line Buffered)
- 只有遇到换行符
\n
,或者缓冲区满了,才flush
(刷新到设备)。例如:stdout
,而且是 连接到终端(屏幕) 时。 - 为什么这么设计? ➔ 人习惯一行一行看输出,比如提示、菜单。
3. 全缓冲(Fully Buffered)
- 要等到缓冲区塞满了(比如 4KB),才刷新到设备。例如:往文件写数据(文件 I/O)
- 为什么这么设计? ➔ 文件操作频繁,小块数据浪费资源,聚集起来一次性写最省!
4. 缓冲区刷新的时机
刷新操作 | 说明 |
---|---|
遇到换行符 | 行缓冲场景 |
缓冲区满 | 全缓冲、行缓冲场景 |
手动调用 fflush(FILE *fp) |
主动要求刷新 |
程序正常结束时 | exit() 会自动 flush 所有打开的流 |
文件流关闭时 | 调用 fclose() 会刷新并关闭流 |
示例代码
1. 标准输出行缓冲示例:
1 |
|
2. 手动刷新缓冲区示例:
1 |
|
5. 后续语言(C++等)流式封装
C++ 的流(iostream):cout
、cin
就是对标准输入输出的面向对象封装。本质上也是自带缓冲区的!
流对象 | 缓冲模式 |
---|---|
cout |
行缓冲 |
cin |
行缓冲 |
例如:
1 |
|
C++标准库自动帮我们管理了:
- 格式化处理(重载
<<
操作符) - 缓冲区管理(什么时候 flush)
所以,不管是 C、C++、Python、Java……只要涉及到「格式化输出」+「IO 效率」的问题,背后一定都有缓冲区!只是封装层次不同,隐藏细节不同而已。
缓冲区=效率神器,格式化=顺序保障,流=高级封装。三者互相配合,共同提升程序性能和输出正确性!
6. 缓冲区在哪?(内存位置)
- 缓冲区是在 用户态内存(程序进程的虚拟内存空间里)。
- 由标准 C 库(glibc)自己在后台维护,比如
FILE
结构体内部就有指针指向缓冲区。
简要结构(示意):
1 | struct _IO_FILE |
7. 系统级缓冲区(内核缓冲区)
除了上面讲的「C 语言库缓冲区」,Linux 内核 也有自己的「内核缓冲区」:
- 当使用
write(fd, buf, len)
时- 其实只是把数据拷贝到 内核态缓冲区(Page Cache)。
- 并不是立刻真正落到磁盘。
- 真正落盘(
Flush
到磁盘) ➔ 要靠fsync()
、sync()
或者内核自己异步刷盘。
8. C 语言缓冲区 vs 内核缓冲区
特点 | C 语言库缓冲区(用户态缓冲) | 系统内核缓冲区(内核态缓冲) |
---|---|---|
属于 | 用户空间 | 内核空间 |
负责 | 提高用户态小块 I/O 效率 |
提高系统磁盘 I/O 效率 |
刷新操作 | fflush(FILE*) |
fsync(int fd) |
刷新时机 | 行满/换行/手动/程序结束 | 写缓存异步、fsync 手动同步 |
举例 | printf 的缓存、scanf 缓存 |
write 到文件、read 从文件 |
说明 | 属于标准库 stdio 层 |
由操作系统控制 |
C 语言缓冲区 是为了 减少用户态到内核态的系统调用次数,
内核缓冲区 是为了 减少磁盘操作的次数。
两者分工明确,各司其职,一起大大提高了 程序和系统整体性能!
9. 重定向后缓冲策略如何变化?
当我们将标准输出 stdout
重定向到一个 文件 时,缓冲模式会从行缓冲变为全缓冲。
1. 实验证明
1 |
|
现在改一下输出到文件:
1 | ./a.out > temp.txt |
你会发现:程序 sleep
完后,temp.txt
才会出现内容! 因为是“全缓冲”,printf
输出被缓存在内存中,直到程序结束才 flush
到文件!
1 |
|
观察:
- 如果不加
fflush()
,在sleep
前的输出内容是否立即出现在test.txt
? - 加了
fflush()
是否立刻输出?
2. 为什么会发生这种变化?
标准 I/O 的缓冲策略是由库函数(如 printf
所依赖的 stdio
层)自动根据 输出目标类型 决定的:
输出目标 | 默认缓冲模式 |
---|---|
终端(屏幕) | 行缓冲(line-buffered) |
普通文件 | 全缓冲(fully-buffered) |
管道 / socket | 全缓冲(fully-buffered) |
标准错误 stderr | 无缓冲(unbuffered) |
3. 为什么屏幕默认用行缓冲?
- 因为用户在终端交互时,希望立刻看到输出,不能等太久。
- 所以只要遇到换行符
\n
或缓冲区满了,就会刷新。
4. 为什么输出到文件变成全缓冲?
- 写文件不需要实时响应,频繁刷写磁盘 开销很大。
- 所以库会自动采用 更高效的方式:
- 多次
printf()
的内容先拼到缓冲区。 - 缓冲区满或手动调用
fflush()
时才统一写入磁盘。
- 多次
[!NOTE]
了解内容:手动控制缓冲行为
用
setvbuf()
可以手动设置缓冲区行为:
1
2
3 setvbuf(stdout, NULL, _IONBF, 0); // 设置 stdout 为无缓冲(unbuffered)
setvbuf(stdout, NULL, _IOLBF, 0); // 设置 stdout 为行缓冲(line-buffered)
setvbuf(stdout, NULL, _IOFBF, 0); // 设置 stdout 为全缓冲(full-buffered)注意:
setvbuf()
必须在第一次输出之前调用,否则无效!示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
int main()
{
// 设置 stdout 为无缓冲
setvbuf(stdout, NULL, _IONBF, 0); // 立即刷新,适用于日志输出
// 设置 stdout 为行缓冲(默认终端下行为)
// setvbuf(stdout, NULL, _IOLBF, 0);
// 设置 stdout 为全缓冲(适用于文件等)
// setvbuf(stdout, NULL, _IOFBF, BUFSIZ);
}
5. 常见情况
- 输出内容看不到?
1 |
|
解决办法:
1 |
|
- 程序崩溃、日志文件为空?
1 |
|
解决办法:
1 |
|
- 直接设置为无缓冲模式
1 |
|
10. 简易版本的 stdio
库的实现
1 | // 头文件 |
1 | // 函数文件 |
1 | // 测试文件 |