024 基础 IO —— 缓冲区

82 阅读12分钟

缓冲区

相关文章 | CSDN

1. 为什么需要缓冲区?(核心原因)

先出结论:格式化 ➔ 拼接成大数据块 ➔ 缓冲 ➔ 统一输出 ➔ 保证数据连贯,减少系统调用,提高效率!

1. 提高 I/O 效率!

硬件设备(尤其是磁盘、网络、终端)操作 很慢很慢,每次读写都直接操作设备 ➔ 太耗时 ➔ 系统负担重。

所以:

  • 少量多次 ➔ 聚集成大量一次。
  • 把「很多小的写操作」放到内存中,攒到一定量再「统一批量」写入磁盘/屏幕。
  • 减少系统调用(syscall)次数 ➔ 提高整体程序运行效率。

简单比喻:你买菜如果每买一根葱就跑一次超市,累不累?当然要一次性买一堆,装个购物袋带回来!(这就是缓冲思想)

2. 配合格式化

printf("名字:%s, 年龄:%d\n", name, age); 这种格式化输出,本质上做了两件事:

  1. 格式化处理(Format)
    • %s%d 等占位符 ➔ 根据数据类型,把 nameage 这两个变量 格式化成字符串
    • 例如:把整数 23 转成 "23",字符串 "Tom" 保持原样。
  2. 统一输出(Buffer)
    • 把格式化好的整个大字符串(比如 "名字:Tom, 年龄:23\n"统一写入 缓冲区
    • 最后一次性刷出去(flush 到终端/文件)

如果没有缓冲区,结果会很糟糕:

  • 每遇到一个小块格式化的数据就立刻 write() 系统调用 ➔ 系统调用开销非常大(context switch)!
  • 输出内容 碎片化 ➔ 终端打印时出现撕裂、乱码、数据乱序。
  • 多线程程序中可能出现 打印穿插错乱

所以:必须先格式化 → 再统一放入缓冲区 → 再统一刷新输出!这样可以:

  • 保证输出内容的原子性(一整块输出,保持顺序一致性)
  • 提高I/O效率(少系统调用)
  • 减少设备压力

2. C 语言的缓冲区(库缓冲区)

在 C 语言中,当使用 printffprintfscanffread标准 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):coutcin 就是对标准输入输出的面向对象封装。本质上也是自带缓冲区的!

流对象缓冲模式
cout行缓冲
cin行缓冲

例如:

#include <iostream>
using namespace std;

int main()
{
	cout << "Hello";		// 存在缓冲区,暂时不会马上输出
	cout << " World\n";		// 遇到换行符,flush,全部一起输出
	return 0;
}

C++标准库自动帮我们管理了:

  1. 格式化处理(重载 << 操作符)
  2. 缓冲区管理(什么时候 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. 常见情况

  1. 输出内容看不到?
#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;
}
  1. 程序崩溃、日志文件为空?
#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;
}
  1. 直接设置为无缓冲模式
#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;
}