目录
[2.C 语言缓冲区的刷新机制](#2.C 语言缓冲区的刷新机制)
1.C语言的缓冲区与文件缓冲区

文件缓冲区存在于 Linux 内核中,是磁盘 I/O 的缓存层 ,也称为页缓存
C 语言的缓冲区处在用户空间 ,是 C 标准库(如 glibc)提供的用户态 I/O 缓冲。
当程序使用 C 语言文件操作库函数(如fprintf、fwrite)时,数据首先被写入 C 语言缓冲区。
在缓冲区满、调用 fflush() 或文件关闭等条件下,C 标准库会通过系统调用 write() 将数据写入内核的文件缓冲区(页缓存)。
内核会在适当时机(如缓存满、定期同步或调用 fsync() 时)将文件缓冲区中的数据写入磁盘。
读取时,若数据不在内核缓冲区,内核会先从磁盘读入页缓存;C 标准库通过系统调用 read() 将数据从内核缓冲区复制到 C 语言缓冲区,程序再从 C 语言缓冲区中读取数据
2.C 语言缓冲区的刷新机制
-
无缓冲 :数据不经过C标准库缓冲区,直接通过系统调用进行I/O操作。每次写入都会立即触发系统调用,通常用于需要实时输出的场景(如标准错误流 stderr)
-
行缓冲 :当遇到换行符 \n 或缓冲区已满时 ,会自动刷新缓冲区。这通常针对 交互式设备(如终端显示器),以符合人机交互的实时性要求。例如,标准输出流 stdout 在连接到终端时默认采用行缓冲
-
全缓冲 :只有当缓冲区已满或显式调用 fflush() 时才会刷新 。这主要用于普通文件和磁盘I/O,目的是减少系统调用次数,通过批量数据传输来提高I/O效率。普通文件操作默认采用全缓冲模式
-
当进程退出时,所有缓冲模式的文件都会自动刷新(全缓冲文件:自动调用 fflush(),即使缓冲区未满;行缓冲文件:自动刷新;无缓冲文件:本来就无缓冲,无需额外操作)
有关Linux内核中文件缓冲区的刷新机制这里不讲,我们是相信操作系统的,即目前我们暂时假设操作系统会可靠地管理磁盘写入:只要将数据刷新到了内核,数据就可以到硬件中了
3.C语言缓冲区存在的意义
- C标准库的缓冲机制让用户只需调用库函数将数据写入缓冲区即可,后续的刷新操作由库自动管理 ,这大大简化了编程复杂度。同时,通过批量处理减少了系统调用次数,有效提高了I/O效率
- 格式化I/O函数(如 fprintf、fscanf )会将数据在二进制形式与字符形式之间自动转换。例如,写入整数
123时,库函数会将其转换为三个字符'1'、'2'、'3'并存入缓冲区;读取时则执行反向转换。缓冲区机制为这种格式化转换提供了临时的数据存储空间,使字节流的高效处理与数据类型的自动转换得以结合
4.C语言缓冲区存在的位置
每个 FILE 结构体都维护着对应打开文件的状态信息和可能的缓冲区指针
分配的缓冲区可能位于堆、栈或全局数据区,具体取决于实现和用户配置
// 情况1:堆上分配(动态分配)
FILE *fp = fopen("file.txt", "r");
// C库可能在堆上malloc缓冲区
// 但这不是唯一选择
// 情况2:用户提供的缓冲区(位置由用户决定)
char static_buf[BUFSIZ]; // 可能在栈上
static char global_buf[BUFSIZ]; // 可能在全局数据区
char *heap_buf = malloc(BUFSIZ); // 可能在堆上
setbuf(fp, static_buf); // 使用用户指定的缓冲区
// 情况3:静态分配的缓冲区(标准流)
// stdin/stdout/stderr通常使用预分配的静态缓冲区
// 这些在全局数据区,不在堆上
- 缓冲区不是必须的:可以通过 setbuf(fp, NULL) 设为无缓冲
- 缓冲区可能是动态分配的:首次I/O操作时才分配缓冲区
- 默认情况下:每个缓冲模式的文件会有独立的缓冲区
打开10个文件 → 有10个独立的 FILE 对象
打开10个文件 → 有10个独立的文件描述符
打开10个文件 → 最多可能有10个缓冲区(无缓冲模式的文件无缓冲区)
5.解释有关现象
我们先明确一些共识
- printf、fprintf、fwrite等函数将数据写入C标准库缓冲区,这些函数不一定立即封装系统调用 ,而是根据缓冲策略在适当时机 (缓冲区满、行缓冲遇到\n、调用fflush()或文件关闭时)才调用 write() 系统调用
- 系统调用 write() 和 read() 直接将数据从用户空间传输到内核空间,进入内核的页缓存(所有文件共享的缓冲区)
- C语言缓冲区刷新的本质是:将用户空间缓冲区中的数据通过 write() 系统调用复制到内核页缓存。这是一个用户态到内核态的数据拷贝过程
- fflush() 通过调用 write() 系统调用,将 C 标准库缓冲区中的数据写入内核,从而完成缓冲区的刷新
- 从操作系统视角来看,用户空间包含进程地址空间中的所有内容。因此:FILE 结构体在进程内存中 → 属于用户空间;C语言缓冲区在进程内存中 → 属于用户空间;进程本身运行在用户态 → 属于用户
(1)
int main()
{
const char* mesg = "hello fwrite\n";
const char* str = "hello write\n";
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
fwrite(mesg, 1, strlen(mesg), stdout);
write(1, str, strlen(str));
return 0;
}

行刷新策略,遇到\n直接刷新,所以调用一次输出一次
(2)

还是上述代码,当重定向至 log.txt 文件时,刷新策略变为了全缓冲,这显然是为了较少系统调用次数,提高效率,所以 write 调用最先写入 log.txt(无缓冲,立即写入文件),其余的按调用次序写入
(3)
8 int main()
9 {
10 const char* mesg = "hello fwrite\n";
11 const char* str = "hello write\n";
12 printf("hello printf\n");
13 fprintf(stdout, "hello fprintf\n");
14 fwrite(mesg, 1, strlen(mesg), stdout);
15 write(1, str, strlen(str));
16
17 fork();
18 return 0;
19 }

代码最后创建了子进程,但是在这之前语言层面的缓冲区已经按照行刷新的策略清空完毕,进程退出时的刷新操作并未对数据造成任何影响,所以此时的 fork 调用与否,结果一致
(4)

还是上述代码,当重定向至 log.txt 文件时,刷新策略变为了全缓冲 ,这显然是为了较少系统调用次数,提高效率,所以 write 调用最先写入 log.txt(无缓冲,立即写入文件),其余的按调用次序写入,但是当进程退出时,对于缓冲区的刷新相当于修改数据 ,此时触发写时拷贝机制,父子进程各自拥有了相同内容的缓冲区,进程退出前,缓冲区内容刷新到内核,最终写入 log.txt 文件当中,结果呈现出两次打印
(5)
8 int main()
9 {
10
11 const char* mesg = "hello fwrite\n";
12 const char* str = "hello write\n";
13 printf("hello printf\n");
14 fprintf(stdout, "hello fprintf\n");
15 fwrite(mesg, 1, strlen(mesg), stdout);
16 close(1);
17 write(1, str, strlen(str));
28 return 0;
29 }

行缓冲策略使得printf等函数的数据在close(1)之前已刷新到终端,所以成功显示。之后close(1)关闭了文件描述符,导致write()系统调用失败,没有输出
(6)
8 int main()
9 {
10
11 const char* mesg = "hello fwrite";
12 const char* str = "hello write";
13 printf("hello printf");
14 fprintf(stdout, "hello fprintf");
15 fwrite(mesg, 1, strlen(mesg), stdout);
16 write(1, str, strlen(str));
17
18 close(1);
return 0;
}

此时采用的是全缓冲策略,但在调用 close 关闭标准输出文件之前,语言层面的缓冲区并未得到刷新,只有 write 调用因为无缓冲直接写入才最终显示
6.模拟实现简陋版的C文件标准库
Mystdio.h
//#pragma once
#ifndef __MYSTDIO_H__
#define __MYSTDIO_H__
#include <string.h>
#define FLUSH_NOW 1
#define FLUSH_LINE 2
#define FLUSH_ALL 4
#define SIZE 1024
typedef struct IO_FILE
{
int fileno;//文件描述符
int flag; //刷新策略
//char inbuff[SIZE];
//int in_pos;
char outbuff[SIZE];//输出缓冲区
int out_pos;//最后一个有效字符的下一个位置
}_FILE;
_FILE* _fopen(const char* filename, const char* flag);
int _fwrite(_FILE* fp, const char* s, int len);
void _fclose(_FILE* fp);
#endif
(1)使用条件编译或者 #pragma once 防止头文件被重复包含
(2)定义 _FILE 结构体,包含文件描述符、刷新标志位、输出缓冲区
(3)声明 _fopen、_fwrite、_fclose 函数
Mystdio.c
#include "Mystdio.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#define FILE_MODE 0666
包含相关头文件,宏定义文件权限
// "w", "a", "r"
_FILE * _fopen(const char*filename, const char *flag)
{
assert(filename);
assert(flag);
int f = 0;
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));
if(fp == NULL) return NULL;
fp->fileno = fd;
//fp->flag = FLUSH_LINE;
fp->flag = FLUSH_ALL;
fp->out_pos = 0;
return fp;
}
(1)只针对 "w""r""a"三种文件打开方式简要实现
(2)首先断言文件路径、打开方式不为空,然后根据打开方式调用 open 函数
(3)成功打开文件之后,为 _FILE 结构体申请空间,然后初始化文件描述符与刷新策略
(4)然后返回 _FILE* 指针
int _fwrite(_FILE *fp, const char *s, int len)
{
// "abcd\n"
memcpy(&fp->outbuff[fp->out_pos], s, len); // 没有做异常处理, 也不考虑局>部问题
fp->out_pos += len;
if(fp->flag&FLUSH_NOW)
{
write(fp->fileno, fp->outbuff, fp->out_pos);
fp->out_pos = 0;
}
else if(fp->flag&FLUSH_LINE)
{
if(fp->outbuff[fp->out_pos-1] == '\n'){ // 不考虑其他情况
write(fp->fileno, fp->outbuff, fp->out_pos);
fp->out_pos = 0;
}
}
else if(fp->flag & FLUSH_ALL)
{
if(fp->out_pos == SIZE){
write(fp->fileno, fp->outbuff, fp->out_pos);
fp->out_pos = 0;
}
}
return len;
}
(1)首先将要写入的内容拷贝到语言层面的缓冲区,即 outbuff 中
(2)然后根据缓冲策略调用 write 函数将数据写入到内核中
(3)内容成拷贝到语言层面的缓冲区就可以返回写入字节数了
void _fflush(_FILE *fp)
{
if(fp->out_pos > 0){
write(fp->fileno, fp->outbuff, fp->out_pos);
fp->out_pos = 0;
}
}
_fflush 内部调用了 write 函数
void _fclose(_FILE *fp)
{
if(fp == NULL) return;
_fflush(fp);
close(fp->fileno);
free(fp);
}
进程退出前刷新一下语言层面的缓冲区,然后关闭内核对应文件对象,释放 _FILE 结构体
main.c
#include"Mystdio.h"
#include<unistd.h>
#define Myfile "test.txt"
int main()
{
//打开文件
//_FILE* fp = _fopen(Myfile, "w");
_FILE* fp = _fopen(Myfile, "a");
if(fp == NULL) return 1;
//向文件写入
const char* mesg = "hello world\n";
int cnt = 10;
while(cnt--)
{
_fwrite(fp, mesg, strlen(mesg));
sleep(1);
}
//fflush(fp)
//关闭文件
_fclose(fp);
return 0;
}
调用并验证函数实现,这样我们就能够大致了解C语言文件操作函数的原理了