缓冲区
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. 标准输出行缓冲示例:
#include <stdio.h>
int main()
{
printf("Hello, "); // 暂存在行缓冲区
sleep(2); // 暂停2秒,屏幕还没输出!
printf("World!\n"); // 遇到换行符,flush 输出
return 0;
}
2. 手动刷新缓冲区示例:
#include <stdio.h>
int main()
{
printf("处理中..."); // 没有 \n,屏幕不会立刻看到
fflush(stdout); // 手动刷新,立刻输出到屏幕
sleep(3); // 继续做别的事情
printf("完成!\n");
return 0;
}
5. 后续语言(C++等)流式封装
C++ 的流(iostream):cout、cin 就是对标准输入输出的面向对象封装。本质上也是自带缓冲区的!
| 流对象 | 缓冲模式 |
|---|---|
cout | 行缓冲 |
cin | 行缓冲 |
例如:
#include <iostream>
using namespace std;
int main()
{
cout << "Hello"; // 存在缓冲区,暂时不会马上输出
cout << " World\n"; // 遇到换行符,flush,全部一起输出
return 0;
}
C++标准库自动帮我们管理了:
- 格式化处理(重载
<<操作符) - 缓冲区管理(什么时候 flush)
所以,不管是 C、C++、Python、Java……只要涉及到「格式化输出」+「IO 效率」的问题,背后一定都有缓冲区!只是封装层次不同,隐藏细节不同而已。
缓冲区=效率神器,格式化=顺序保障,流=高级封装。三者互相配合,共同提升程序性能和输出正确性!
6. 缓冲区在哪?(内存位置)
- 缓冲区是在 用户态内存(程序进程的虚拟内存空间里)。
- 由标准 C 库(glibc)自己在后台维护,比如
FILE结构体内部就有指针指向缓冲区。
简要结构(示意):
struct _IO_FILE
{
unsigned char* _IO_buf_base; // 缓冲区起始地址
unsigned char* _IO_buf_end; // 缓冲区结束地址
...
};
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. 实验证明
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
printf("打印到屏幕(终端)前先睡5秒...\n");
sleep(5); // 打印了才 sleep,说明是“行缓冲”
return 0;
}
现在改一下输出到文件:
./a.out > temp.txt
你会发现:程序 sleep 完后,temp.txt 才会出现内容! 因为是“全缓冲”,printf 输出被缓存在内存中,直到程序结束才 flush 到文件!
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("准备输出...\n");
sleep(5);
printf("Hello, File!\n");
// fflush(stdout); // 去掉这行试试!
}
// 运行方式:./a.out > test.txt
观察:
- 如果不加
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()可以手动设置缓冲区行为:setvbuf(stdout, NULL, _IONBF, 0); // 设置 stdout 为无缓冲(unbuffered) setvbuf(stdout, NULL, _IOLBF, 0); // 设置 stdout 为行缓冲(line-buffered) setvbuf(stdout, NULL, _IOFBF, 0); // 设置 stdout 为全缓冲(full-buffered)注意:
setvbuf()必须在第一次输出之前调用,否则无效!示例代码:#include <stdio.h> int main() { // 设置 stdout 为无缓冲 setvbuf(stdout, NULL, _IONBF, 0); // 立即刷新,适用于日志输出 // 设置 stdout 为行缓冲(默认终端下行为) // setvbuf(stdout, NULL, _IOLBF, 0); // 设置 stdout 为全缓冲(适用于文件等) // setvbuf(stdout, NULL, _IOFBF, BUFSIZ); }
5. 常见情况
- 输出内容看不到?
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("日志输出:程序开始执行...\n"); // 重定向后不会立刻写入文件(全缓冲)
sleep(10); // 程序“暂停”10 秒,但日志还未写入
printf("日志输出:程序即将退出。\n");
return 0;
}
// 注意:查看重定向文件时:在程序运行期间它是空的,只有等程序结束后才出现内容。
解决办法:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("日志输出:程序开始执行...\n");
fflush(stdout); // 手动刷新,确保写入文件
sleep(10);
printf("日志输出:程序即将退出。\n");
fflush(stdout); // 最好在关键日志后都刷新一次
return 0;
}
- 程序崩溃、日志文件为空?
#include <stdio.h>
int main()
{
printf("准备执行崩溃逻辑...\n");
int* p = NULL;
*p = 42; // 故意造成段错误,程序崩溃
return 0;
}
解决办法:
#include <stdio.h>
int main()
{
printf("准备执行崩溃逻辑...\n");
fflush(stdout); // 手动刷新,日志及时写入
int* p = NULL;
*p = 42;
return 0;
}
- 直接设置为无缓冲模式
#include <stdio.h>
#include <unistd.h>
int main()
{
setvbuf(stdout, NULL, _IONBF, 0); // 设置 stdout 为无缓冲模式
printf("程序正在执行...\n"); // 会立刻写入
sleep(10);
printf("程序执行结束。\n");
return 0;
}
10. 简易版本的 stdio 库的实现
// 头文件
//#pragma once // 可选,防止头文件重复包含
#ifndef __MYSTDIO_H__ // 防止头文件重复包含(include guard)
#define __MYSTDIO_H__
#include <string.h> // 用于处理字符串函数,strcmp(), memcpy()
#define SIZE 1024 // 缓冲区大小:1KB
// 缓冲刷新策略(flush strategy)标志
#define FLUSH_NOW 1 // 每次写入立即刷新(立即写入文件)
#define FLUSH_LINE 2 // 按行刷新(检测到换行符时刷新)
#define FLUSH_ALL 4 // 缓冲区满了才刷新(默认)
// 自定义文件结构体,模拟 FILE 类型
typedef struct IO_FILE
{
int fileno; // 文件描述符
int flag; // 刷新策略
//char inbuffer[SIZE]; // 输入缓冲区(未实现)
//int in_pos; // 输入缓冲位置指针
char outbuffer[SIZE]; // 输出缓冲区(output buffer)
int out_pos; // 当前写入缓冲区的位置
}_FILE;
// 函数声明部分
_FILE* _fopen(const char* filename, const char* flag); // 打开文件(模拟 fopen)
int _fwrite(_FILE* fp, const char* s, int len); // 写入字符串到缓冲区(模拟 fwrite)
void _fclose(_FILE* fp); // 关闭文件(模拟 fclose)
#endif
// 函数文件
#include "mystdio.h"
#include <sys/types.h> // 基本系统数据类型定义 size_t
#include <sys/stat.h> // 文件权限模式常量 S_IRWXU、S_IRWXG、S_IRWXO
#include <fcntl.h> // 文件控制选项 O_CREAT、O_WRONLY、O_APPEND、O_RDONLY
#include <stdlib.h> // malloc()、free()
#include <unistd.h> // read()、write()、close()
#include <assert.h> // assert() 断言检查
#define FILE_MODE 0666 // 默认文件权限 rw-rw-rw-
// 打开文件:支持 "w"(写入)、"a"(追加)、"r"(只读)
_FILE* _fopen(const char* filename, const char* flag)
{
assert(filename); // 确保文件名不为空
assert(flag); // 确保模式不为空
int f = 0; // 打开文件使用的标志位(flags)
int fd = -1; // 文件描述符初始化为非法值
if (strcmp(flag, "w") == 0)
{
f = (O_CREAT | O_WRONLY | O_TRUNC); // 创建+只写+截断旧内容
fd = open(filename, f, FILE_MODE);
}
else if (strcmp(flag, "a") == 0)
{
f = (O_CREAT | O_WRONLY | O_APPEND); // 创建+只写+追加写入
fd = open(filename, f, FILE_MODE);
}
else if (strcmp(flag, "r") == 0)
{
f = O_RDONLY; // 只读模式
fd = open(filename, f);
}
else
{
return NULL; // 不支持的模式,返回空指针
}
if (fd == -1)
{
return NULL; // 打开失败
}
_FILE* fp = (_FILE*)malloc(sizeof(_FILE)); // 为 _FILE 结构体分配内存
if (fp == NULL)
{
return NULL; // 内存分配失败
}
fp->fileno = fd; // 设置文件描述符
//fp->flag = FLUSH_LINE; // 可选行刷新模式
fp->flag = FLUSH_ALL; // 默认采用缓存满再写入
fp->out_pos = 0; // 输出缓冲区指针置 0
return fp;
}
int _fwrite(_FILE* fp, const char* s, int len) // 写入函数:将字符串写入到自定义缓冲区中
{
// 将数据从字符串拷贝到输出缓冲区(假设缓冲足够)
memcpy(&fp->outbuffer[fp->out_pos], s, len); // 无边界检测(简化实现)
fp->out_pos += len;
if (fp->flag & FLUSH_NOW) // 判断刷新策略
{
write(fp->fileno, fp->outbuffer, fp->out_pos); // 立即写入所有缓冲数据到文件
fp->out_pos = 0;
}
else if (fp->flag & FLUSH_LINE) // 按行刷新
{
if (fp->outbuffer[fp->out_pos - 1] == '\n') // 如果最后一个字符是换行符,则刷新
{
write(fp->fileno, fp->outbuffer, fp->out_pos);
fp->out_pos = 0;
}
}
else if (fp->flag & FLUSH_ALL) // 按缓存满刷新
{
if (fp->out_pos == SIZE) // 缓冲区满时刷新(一次写入 SIZE 字节)
{
write(fp->fileno, fp->outbuffer, fp->out_pos);
fp->out_pos = 0;
}
}
return len; // 返回写入的长度
}
// 手动刷新函数(模拟 fflush)
void _fflush(_FILE* fp)
{
if (fp->out_pos > 0)
{
write(fp->fileno, fp->outbuffer, fp->out_pos); // 写入缓冲数据
fp->out_pos = 0;
}
}
// 关闭文件(释放资源)
void _fclose(_FILE* fp)
{
if (fp == NULL) return;
_fflush(fp); // 关闭前确保缓冲区已写入
close(fp->fileno); // 关闭文件描述符
free(fp); // 释放分配的内存
}
// 测试文件
#include "mystdio.h"
#include <unistd.h> // 用于 sleep() 函数
#define myfile "test.txt" // 要写入的测试文件名称
int main()
{
// 打开文件,以追加模式 ("a") 打开
_FILE* fp = _fopen(myfile, "a");
if (fp == NULL)
{
return 1; // 打开失败则退出
}
const char* msg = "hello world\n"; // 写入的字符串内容
int cnt = 10; // 循环次数
while (cnt)
{
_fwrite(fp, msg, strlen(msg)); // 每次写入一行
// _fflush(fp); // 如需每次立即写入可手动刷新
sleep(1); // 每 1 秒写一次
cnt--;
}
_fclose(fp); // 写入完毕后关闭文件,释放资源
return 0;
}