前言
本文深入探讨 Linux 系统中用户缓冲区的概念与工作原理。通过分析 C 语言文件接口(printf、fprintf、fwrite)与系统调用接口(write)的区别,揭示缓冲区在文件 IO 中的重要作用。
一、核心概念
1.1 两种缓冲区
Linux 系统中存在两个层面的缓冲区:
Linux 系统中存在用户缓冲区和内核缓冲区。C 语言库函数如 printf 先写入用户缓冲区,系统调用 write 直接写入内核缓冲区。重定向会改变缓冲策略,显示器为行缓冲,文件为全缓冲。fork 操作会导致未刷新的用户缓冲区被复制,造成数据重复。理解缓冲区刷新机制对调试 IO 问题和优化性能至关重要。

本文深入探讨 Linux 系统中用户缓冲区的概念与工作原理。通过分析 C 语言文件接口(printf、fprintf、fwrite)与系统调用接口(write)的区别,揭示缓冲区在文件 IO 中的重要作用。
Linux 系统中存在两个层面的缓冲区:
| 类型 | 位置 | 归属 | 刷新机制 |
|---|---|---|---|
| 用户缓冲区 | 用户空间 | C 语言库 FILE 结构体 | 由库函数控制 |
| 内核缓冲区 | 内核空间 | 操作系统 | 由 OS 控制 |
// C 语言库函数接口
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
fwrite(str, len, 1, stdout);
// 系统调用接口
write(1, str, len);
关键区别:
size_t ret1 = fwrite(str, len, 1, stdout); // 返回:写入的块数
ssize_t ret2 = write(1, str, len); // 返回:写入的字节数
| 模式 | 触发条件 | 典型场景 |
|---|---|---|
| 无缓冲 | 立即刷新 | fflush() 函数 |
| 行缓冲 | 遇到\n刷新 | 显示器输出 |
| 全缓冲 | 缓冲区满时刷新 | 普通文件写入 |
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
fwrite("hello fwrite\n", 13, 1, stdout);
write(1, "hello write\n", 12);
结果:四个函数都正常输出到屏幕
解释:显示器采用行缓冲,遇到\n立即刷新。
# 同样的代码,但执行时重定向:./code1 > log.txt
结果:log.txt 中包含 4 行输出
解释:重定向后,输出目标从显示器变为普通文件,但仍能正常写入。
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
fwrite("hello fwrite\n", 13, 1, stdout);
write(1, "hello write\n", 12);
fork(); // 创建子进程
结果:每个消息只打印一次
解释:由于是行缓冲,数据在 fork 前已刷新到内核,用户缓冲区为空。
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
fwrite("hello fwrite\n", 13, 1, stdout);
write(1, "hello write\n", 12);
fork(); // 创建子进程
// 执行时重定向:./test > log.txt
结果:
hello write hello printf hello fprintf hello fwrite hello printf hello fprintf hello fwrite
详细解释:
验证代码(观察缓冲区刷新时机):
printf("hello printf\n");
sleep(1);
fprintf(stdout, "hello fprintf\n");
sleep(1);
fwrite("hello fwrite\n", 13, 1, stdout);
sleep(1);
write(1, "hello write\n", 12);
sleep(2);
fork();
配合监控脚本:
while :; do cat log.txt; sleep 1; echo "---"; done
printf("hello printf"); // 无换行符
fprintf(stdout, "hello fprintf");
fwrite("hello fwrite", 12, 1, stdout);
close(1); // 关闭 stdout
结果:屏幕没有任何输出
解释:
write(1, "hello write", 11); // 无换行符
close(1);
结果:正常输出
解释:
FILE 结构体(用户空间)
├── fd(文件描述符)
├── 缓冲区指针 ──────→ 堆空间(用户缓冲区)
├── 缓冲区大小
└── 其他维护信息
FILE* fp = fopen("test.txt", "w");
// fp 指向 malloc 分配的结构体
// 结构体中包含指向堆上缓冲区的指针
1. 提高效率
无缓冲:写 100 次 → 100 次系统调用
有缓冲:写 100 次 → 1 次系统调用(批量刷新)
2. 支持格式化
printf("%d", 123); // 将整数转换为字符流
scanf("%d", &a); // 将字符流转换为整数
缓冲区承担数据格式转换的任务。
┌─────────────────────────────────────────────────────┐
│ 用户程序 │
│ │
│ printf/fprintf/fwrite → 用户缓冲区(FILE 中) │
│ ↓ │
│ fflush() │
│ ↓ │
└──────────────────────────────┼──────────────────────┘
│ write 系统调用
↓
┌─────────────────────────────────────────────────────┐
│ 操作系统内核 │
│ │
│ 内核缓冲区 │
│ ↓ │
│ 刷新策略(OS 控制) │
│ ↓ │
└──────────────────────────────┼──────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ 硬件设备 │
│ (显示器/磁盘文件/网络) │
└─────────────────────────────────────────────────────┘
printf("hello\n")
↓
写入用户缓冲区
↓
遇到\n → 触发刷新
↓
调用 write(fd, "hello\n", 6)
↓
数据进入内核缓冲区
↓
OS 按策略刷新到硬件

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online