引言:
以一个常见的问题开始:"你是否曾遇到过
printf
打印的日志没有立即出现在屏幕上,但在程序崩溃后却又看到了?又或者,用write
系统调用时,数据却立刻显示了?这背后其实是缓冲区在作怪。核心观点: 缓冲区是提升I/O效率的关键机制,理解它能帮助你写出性能更高、行为更可预测的程序。
本篇目标: 本文将深入剖析Linux C语言中缓冲区的类型、工作原理、管理方法以及常见坑点以及自己实现的简单的c语言缓冲区。
第一部分:缓冲区是什么?为什么需要它?
直接从定义上来解释或许有些抽象,那么不妨来举一个我们生活中的例子,来把缓冲区抽象的概念具体化,比如,在生活中,我们小区都会有对应的快递中心或者说是快递点,那么这个快递点会因为你的一个包裹就发一次车吗?答案是不会,因为,得等到收集到一定数量后统一发送,来减少运输次数和成本。
定义:在内存中开辟的一块区域,用作应用程序与内核之间的数据中转站。
为什么需要缓冲区呢?当我们进行一次系统调用的时候,如write需要从用户态切换为内核态,这是非常耗时的操作,那么我们有了缓冲区就可以将多次小的数据写入结合一次大的系统调用,极大提升了效率。
匹配速度差异:
CPU和内存的速速远远高于磁盘I/O,缓冲区允许程序快速将数据放入到内存后立即返回,继续执行,由内核在后台处理实际的I/O操作,实现了异步操作的效果。
第二部分:C语言中的缓冲类型(核心内容):
1、行缓冲(_IOLBF):在遇到换行符'\n'的时候,立即刷新缓冲区;缓冲区满的时候,刷新缓冲区;程序正常终止的时候,刷新所有缓冲区;任何时候从无缓冲的流中读取数据,或从行缓冲的流中读取数据,所有待输出的行缓冲流会被刷新。
那么这样做的好处就是可以保证用户及时看到每一行的输出。
2、全缓冲(_IOFBF):从字面上理解就是,当缓冲区被填满的时候,才执行实际的I/O操作(如调用write系统调用)。
典型应用就是:
操作普通文件时(例如使用fopen,fread,fwrite)默认采用全缓冲。
那么全缓冲的优势就在于效率最高,系统调用次数最少。
3、无缓冲(_IONBF):(有时候也称为立即刷新):
数据会被输出,不经过缓冲区。
典型应用就是:
标准错误stderr默认通常是无缓冲的,以确保错误 信息能被立刻看到,即使程序即将崩溃。
优点:即时性高。
第三部分:如何管理和控制缓冲区:
1、自动刷新实际:
第一,缓冲区满了,那么就会进行刷新。
第二,程序正常结束了,也会刷新缓冲区(return,或者exit() )。
第三,进程正常结束了,也会自动刷新其打开文件的缓冲区。
2、手动刷新:
也就是我们自己来刷新缓冲区,常用的方法有:调用fflush函数。
int fflush(FILE*stream)
作用:强制将指定流缓冲区中的内容写入到内核中。
3、显式设置缓冲模式:
那么如何来显式设置缓冲模式呢?那么就不得不得到setbuf函数了。
int setbuf(FILE*stream,char* buf,int mode,size_t size)
第一个参数:
指向要设置缓冲模式的文件流指针(常见的就是stdin和stdout以及stderr)。
第二个参数:
指向自定义缓冲区的地址,决定缓冲区的来源,如果为NULL,那么由c标准库自动分配一块默认大小的缓冲区,无需手动管理。
若为非NULL,如我们自己写的一个字符数组char my_buf[1024],那么此时缓冲区由用户提供,需要注意的是一定要保证避免野指针。
mode:
_IOFBF(F是full充满的首字母)。全缓冲
_IOLBF(L是line这个单词的首字母,是行的意思)。行缓冲
_IONBF(N是now,也就是现在的意思的首字母。无缓冲(立即刷新)
那么接下来,我们将用一段代码来进行理解setbuf函数如何使用。
cpp
#include <stdio.h>
#include <unistd.h>
int main()
{
// 测试无缓冲模式
printf("设置标准输出为无缓冲模式\n");
if (setvbuf(stdout, NULL, _IONBF, 0) != 0)
{
perror("setvbuf _IONBF error");
return 1;
}
printf("无缓冲模式下,这句话会立即输出\n");
// 测试行缓冲模式
printf("\n设置标准输出为行缓冲模式\n");
if (setvbuf(stdout, NULL, _IOLBF, 0) != 0)
{
perror("setvbuf _IOLBF error");
return 1;
}
printf("行缓冲模式下,这句话会在遇到换行符或缓冲区满时输出,这里");
printf("加上换行符后输出\n");
// 测试全缓冲模式(仅新增 Linux 下的延迟,验证 fflush 作用)
printf("\n设置标准输出为全缓冲模式\n");
char buf[1024];
if (setvbuf(stdout, buf, _IOFBF, sizeof(buf)) != 0)
{
perror("setvbuf _IOFBF error");
return 1;
}
// 原有打印语句保留,新增等待提示
printf("全缓冲模式下,这句话会在缓冲区满或显式刷新(如fflush)时输出,现在先不刷新\n");
// 新增第一步:Linux 下延迟 5 秒(验证"未 fflush、程序未终止时不输出")
printf("提示:接下来等待 5 秒(此时上面那句话应未输出,因未 fflush 且程序没终止)...\n");
sleep(5); // Linux 专用延迟函数(参数为秒,直接延迟 5 秒)
// 原有 fflush 调用保留,新增刷新提示
printf("等待结束,开始调用 fflush 显式刷新...\n");
fflush(stdout); // 显式刷新缓冲区
// 新增第二步:再延迟 5 秒(验证"fflush 后即使程序没终止也已输出")
printf("fflush 调用完成,再等待 5 秒(此时上面所有语句应已输出,证明是 fflush 生效)...\n");
sleep(5); // 再次延迟,观察输出状态
return 0;
}
在上述的代码中我每一行代码都做了详细的解析,那么我将打印输出结果来证明上面所讲的结论是正确的。

提到缓冲区,在进程终止那里我们讲了exit和_exit函数。
exit()是标准库函数,会刷新所有标准I/O缓冲区,然后调用退出系统调用。
_exit()是系统调用,直接终止进程,不会刷新任何缓冲区,会导致缓冲区中的数据丢失。
第四部分:系统调用与标准I/O库的缓冲:
这里是很容易混淆的,所以必须讲清楚。
标准I/O函数:如printf,fgets,fputc,它们操作的对象都是FILE*结构体,是c语言提供的,带有用户态的缓冲区。后续我们会自己实现一个简单的FILE结构体,来更好地对这个缓冲区的概念有个更清晰的理解。
系统调用:如write,read,它们操作的对象都是文件描述符(fd),如STDOUT_FILENO。它们通常是无缓冲的(这里指的是用户态无缓冲,实际上内核里仍有缓存)。
当你调用write(fd,data,size)时,数据通常会立刻从用户空间拷贝到内核空间的缓冲区中,然后由内核决定何时写入到磁盘,但是从用户程序的角度上来看,此次调用已经完成了,但是其实后面的过程都是由操作系统自己来管理或者说是控制的。
第五部分(深入讲解):系统调用、文件描述符和内核缓冲区的本质:
一、文件描述符:
文件描述符是什么?它不仅仅只是一个数字。
文件描述符是一个非负整数,这是进程级资源表的索引。
那么这个文件描述符和内核缓冲区之间有什么联系?
整个机制建立在三个核心的内核数据结构上,它们通过指针层层相连、
1、task_struct(进程描述符)
内核中代表一个进程。它包含一个指针->files。
2**、files_struct,进程的文件打开表:**
由tast_struct->files指向,核心是一个指针数组。
struct file*fd_array[NR_OPEN_DEFAULT].
文件描述符(fd)的本质:就是这个fd_array的整数索引。fd的值就是数组的下标,比如说fd=3就是fd_array[3]。
3、file(内核文件对象)
fd_array[fd]数组槽位中存储的指针,指向的就是这个结构
这里面包含什么呢?
1、f_pos:当前文件的读写偏移量。
2、f_mode:文件的打开模式(读、写、追加)。3、f_op:一个指向file_operations结构体的指针,该结构体包含了指向具体驱动函数的函数指针(如.read, .write )。
4、f_path:包含一个指向dentry(目录项)的指针,进而关联到文件的inode。
5、address_space&page_cache(地址空间与页缓存)
内核缓冲区的本质:address_space是内核用于管理文件页缓存的核心数据结构。
一个被打开的文件,其数据内容就缓存在这些strcu page(物理内存页)中,由address_space统一进行管理。
总结如下:进程P
->task_struct
->files_struct
->fd_array[fd]
->struct file
->inode
->address_space
->页缓存(物理内存页)
为了更好地使大家理解这个过程,我们就拿write函数来举例。
我们通过一次
write(fd, buf, count)
系统调用的完整流程,来理解三者的协作关系。这个过程体现了Linux的"一切皆文件"和"抽象与隔离"的设计哲学。步骤 1:用户发起请求
进程调用
write(3, user_buf, 100)
。这里的3
就是文件描述符fd
,user_buf
是用户空间的内存地址。步骤 2:陷入内核
CPU执行特殊指令,硬件接管,从用户态切换到内核态。控制权交给内核中
write
的系统调用处理程序。步骤 3:解析"票据"(核心步骤)
内核拿到
fd = 3
。它现在需要知道这个数字代表什么。内核查找当前进程的文件描述符表(一个数组),以
3
为下标,找到表中对应的那个元素。这个元素是一个指向内核内部文件对象(file object) 的指针。至此,
fd
的使命已完成。它作为一个简单的索引,成功地将系统的请求路由到了正确的内核对象上。步骤 4:权限与安全检查
内核检查找到的"文件对象":进程是否有写入权限?
fd
是否指向一个有效的打开的文件?如果检查失败,返回错误。步骤 5:操作内核缓冲区(核心步骤)
这是与"内核缓冲区"交互的关键。内核不会直接把
user_buf
中的数据写到磁盘上。内核将数据从用户空间的user_buf
拷贝到内核空间的Page Cache(内核缓冲区) 中。这个缓冲区与fd
所指向的文件相关联。
write
系统调用在此刻就可以返回了! 它告诉用户进程"数据已成功写入"。但实际上,数据只是从用户缓冲区移动到了内核缓冲区。
最终的逻辑链条是:
用户进程通过系统调用(接口)发起请求。
请求中携带文件描述符(索引)来指定操作目标。
内核解析该索引,找到对应的内部对象。
内核将数据在用户空间和内核缓冲区(缓存代理)之间进行拷贝,而非直接与磁盘交互。
内核在后台管理缓冲区与磁盘的同步。
第四条同样也证明了一点write函数并不是真正的写,而是把数据进行拷贝,与其说写,不如说是拷贝。
第五部分:简单实现一个缓冲区:
my_stdio.c:
cpp
#include "my_stdio.h"
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <errno.h>
#include <stdio.h> // 用于 perror
#include <unistd.h> // 用于 isatty
/* 创建并初始化一个MY_FILE对象 */
MY_FILE *my_fopen(const char *pathname, const char *mode) {
int fd;
int flags = 0;
int my_flags = 0;
mode_t file_mode = 0644; // 默认文件权限
// 解析模式字符串
switch (mode[0]) {
case 'r':
flags = O_RDONLY;
my_flags = MY_FILE_READ;
break;
case 'w':
flags = O_WRONLY | O_CREAT | O_TRUNC;
my_flags = MY_FILE_WRITE;
break;
case 'a':
flags = O_WRONLY | O_CREAT | O_APPEND;
my_flags = MY_FILE_WRITE | MY_FILE_APPEND;
break;
default:
errno = EINVAL;
return NULL;
}
// 处理 '+' 模式 (读写)
if (mode[1] == '+') {
flags = (flags & ~(O_RDONLY | O_WRONLY)) | O_RDWR;
my_flags |= MY_FILE_READ | MY_FILE_WRITE;
}
// 打开文件
fd = open(pathname, flags, file_mode);
if (fd == -1) {
return NULL;
}
// 分配MY_FILE结构体
MY_FILE *file = (MY_FILE *)malloc(sizeof(MY_FILE));
if (file == NULL) {
close(fd);
errno = ENOMEM;
return NULL;
}
// 初始化MY_FILE成员
file->fd = fd;
file->flags = my_flags;
file->err = 0;
file->count = 0;
file->pos = 0;
// 设置缓冲模式
file->mode = MY_IOFBF;
file->size = BUFSIZ; // 通常为8192
// 如果是终端设备,使用行缓冲
if (isatty(fd)) {
file->mode = MY_IOLBF;
file->size = 1024; // 终端使用较小的缓冲区
}
// 分配缓冲区
if (file->mode != MY_IONBF) {
file->buffer = (char *)malloc(file->size);
if (file->buffer == NULL) {
// 分配失败,降级为无缓冲
file->mode = MY_IONBF;
file->size = 0;
file->buffer = NULL;
}
} else {
file->buffer = NULL;
file->size = 0;
}
return file;
}
/* 关闭文件流,释放资源 */
int my_fclose(MY_FILE *stream) {
if (stream == NULL) {
errno = EINVAL;
return EOF;
}
int ret = 0;
// 刷新输出缓冲区
if ((stream->flags & MY_FILE_WRITE) && stream->count > 0) {
if (my_fflush(stream) == EOF) {
ret = EOF;
}
}
// 关闭文件描述符
if (close(stream->fd) == -1) {
ret = EOF;
}
// 释放缓冲区
if (stream->buffer != NULL) {
free(stream->buffer);
}
// 释放MY_FILE结构体
free(stream);
return ret;
}
/* 刷新输出缓冲区 */
int my_fflush(MY_FILE *stream) {
if (stream == NULL) {
errno = EINVAL;
return EOF;
}
// 只对写入模式的文件进行刷新
if (!(stream->flags & MY_FILE_WRITE)) {
errno = EINVAL;
return EOF;
}
if (stream->count == 0) {
return 0; // 缓冲区为空,无需写入
}
// 将缓冲区内容写入文件
ssize_t written = write(stream->fd, stream->buffer, stream->count);
// 处理写入错误
if (written < 0) {
stream->err = 1;
stream->flags |= MY_FILE_ERROR;
return EOF;
} else if (written < (ssize_t)stream->count) {
// 处理部分写入
memmove(stream->buffer, stream->buffer + written, stream->count - written);
stream->count -= written;
errno = EAGAIN;
stream->err = 1;
stream->flags |= MY_FILE_ERROR;
return EOF;
}
// 成功写入全部数据
stream->count = 0;
return 0;
}
/* 写入数据到文件流 */
size_t my_fwrite(const void *ptr, size_t size, size_t nmemb, MY_FILE *stream) {
if (!(stream->flags & MY_FILE_WRITE)) {
errno = EINVAL;
stream->err = 1;
stream->flags |= MY_FILE_ERROR;
return 0;
}
// 无缓冲模式:直接写入
if (stream->mode == MY_IONBF) {
ssize_t written = write(stream->fd, ptr, size * nmemb);
if (written < 0) {
stream->err = 1;
stream->flags |= MY_FILE_ERROR;
return 0;
}
return written / size;
}
const char *data = (const char *)ptr;
size_t total_size = size * nmemb;
size_t written = 0;
while (written < total_size) {
// 计算缓冲区剩余空间
size_t free_space = stream->size - stream->count;
size_t to_copy = total_size - written;
if (to_copy > free_space) {
to_copy = free_space;
}
// 拷贝数据到缓冲区
memcpy(stream->buffer + stream->count, data + written, to_copy);
stream->count += to_copy;
written += to_copy;
// 检查是否需要刷新
int flush_needed = 0;
if (stream->mode == MY_IOLBF) {
// 行缓冲:检查是否有换行符
for (size_t i = stream->count - to_copy; i < stream->count; i++) {
if (stream->buffer[i] == '\n') {
flush_needed = 1;
break;
}
}
} else if (stream->mode == MY_IOFBF && stream->count == stream->size) {
// 全缓冲:缓冲区满时刷新
flush_needed = 1;
}
if (flush_needed) {
if (my_fflush(stream) == EOF) {
// 刷新失败,返回已成功写入的数据量
return written / size;
}
}
}
return nmemb;
}
/* 写入字符串到文件流 */
int my_fputs(const char *str, MY_FILE *stream) {
size_t len = strlen(str);
size_t written = my_fwrite(str, 1, len, stream);
return (written == len) ? 0 : EOF;
}
/* 检查文件错误标志 */
int my_ferror(MY_FILE *stream) {
if (stream == NULL) {
return 0;
}
return stream->err;
}
/* 检查文件结束标志 */
int my_feof(MY_FILE *stream) {
if (stream == NULL) {
return 0;
}
return (stream->flags & MY_FILE_EOF) != 0;
}
my_sdtio.h:
cpp
#ifndef MY_STDIO_H
#define MY_STDIO_H
#include <sys/types.h> // 用于 size_t 等类型
/* 缓冲模式定义 */
#define MY_IOFBF 0 // 全缓冲
#define MY_IOLBF 1 // 行缓冲
#define MY_IONBF 2 // 无缓冲
/* 文件状态标志 */
#define MY_FILE_READ 0x01
#define MY_FILE_WRITE 0x02
#define MY_FILE_EOF 0x04
#define MY_FILE_ERROR 0x08
#define MY_FILE_APPEND 0x10
/* 自定义FILE结构体 */
typedef struct MY_FILE {
int fd; // 对应的文件描述符
char *buffer; // 指向缓冲区块的指针
size_t size; // 缓冲区的总容量
size_t count; // 缓冲区当前已使用的字节数(对于输出缓冲区)
size_t pos; // 缓冲区当前读取位置(对于输入缓冲区)
int mode; // 缓冲模式 (MY_IOFBF, MY_IOLBF, MY_IONBF)
int flags; // 文件状态标志
int err; // 错误标志
} MY_FILE;
/* 函数声明 */
MY_FILE *my_fopen(const char *pathname, const char *mode);
int my_fclose(MY_FILE *stream);
size_t my_fwrite(const void *ptr, size_t size, size_t nmemb, MY_FILE *stream);
int my_fflush(MY_FILE *stream);
int my_fputs(const char *str, MY_FILE *stream);
// 辅助函数声明
int my_ferror(MY_FILE *stream);
int my_feof(MY_FILE *stream);
#endif /* MY_STDIO_H */
test_mystdio.c
cpp
#include "my_stdio.h"
#include<stdio.h>
#include <string.h>
int main() {
// 测试1: 写入文件
printf("测试1: 写入文件\n");
MY_FILE *file = my_fopen("test.txt", "w");
if (file == NULL) {
perror("无法打开文件");
return 1;
}
const char *text = "Hello, World!\nThis is a test of custom I/O buffering.\n";
size_t written = my_fwrite(text, 1, strlen(text), file);
printf("已写入 %zu 字节到文件\n", written);
// 注意:数据可能还在缓冲区中,没有写入磁盘
printf("数据在缓冲区中,尚未写入磁盘...\n");
// 强制刷新缓冲区
my_fflush(file);
printf("已刷新缓冲区,数据应已写入磁盘\n");
my_fclose(file);
printf("文件已关闭\n\n");
// 测试2: 追加到文件
printf("测试2: 追加到文件\n");
file = my_fopen("test.txt", "a");
if (file == NULL) {
perror("无法打开文件");
return 1;
}
my_fputs("This is appended text.\n", file);
my_fclose(file);
printf("追加完成,文件已关闭\n\n");
// 测试3: 读取文件 (简化版,未实现fread)
printf("测试3: 读取文件内容\n");
FILE *stdio_file = fopen("test.txt", "r");
if (stdio_file == NULL) {
perror("无法打开文件");
return 1;
}
char buffer[256];
while (fgets(buffer, sizeof(buffer), stdio_file) != NULL) {
printf("%s", buffer);
}
fclose(stdio_file);
return 0;
}

如上,我们就完成了缓冲区的模拟实现,但请注意,这里的缓冲区只是实现了简单的接口,实际上的缓冲区比这要复杂的多。
综上所述,缓冲区的讲解就到此为止了。