目录

1. 引言

2. 什么是缓冲区?

3. 为什么要引入缓冲区?

4. 标准I/O提供的三种缓冲类型

5. 实验:重定向改变缓冲行为

6. fork 对缓冲区的影响(经典面试题)

7. FILE 结构体的内部实现

8. 内核缓冲区(页缓存)

9. 小结


1. 引言

在使用 printf 输出到显示器时,数据立刻显示;但重定向到文件后,有时需要 fflush 才能写入。这种现象背后是缓冲区机制。本文将详细讲解缓冲区的概念、类型、引入原因,并通过实验和自定义 my_stdio 库来揭示 FILE 结构体的秘密。

2. 什么是缓冲区?

缓冲区是内存中的一块区域,用于临时存储输入或输出数据。根据方向分为输入缓冲区和输出缓冲区。

3. 为什么要引入缓冲区?

  • 减少系统调用次数:系统调用(如 write)需要从用户态切换到内核态,开销较大。通过缓冲,可以将多次小数据量写入合并为一次大数据量写入。

  • 提高效率:内存操作比磁盘I/O快几个数量级。

  • 协调CPU与外设速度:打印机、磁盘等低速设备可以通过缓冲区与CPU解耦。

4. 标准I/O提供的三种缓冲类型

类型 刷新条件 典型场景
全缓冲 缓冲区满、进程正常终止、调用fflush 普通磁盘文件
行缓冲 遇到换行符\n、缓冲区满、进程终止 标准输入输出(终端)
无缓冲 立即输出 stderr(便于错误信息及时显示)

补充: 行缓冲的缓冲区大小通常为1024字节,若未遇到换行符但已写满也会刷新。

5. 实验:重定向改变缓冲行为

代码示例:

c

close(1);
int fd = open("log.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
printf("hello world: %d\n", fd);
close(fd);

现象: log.txt 为空。
原因: 关闭标准输出后,新打开的文件 fd=1,但由于输出目标是磁盘文件,缓冲类型从行缓冲变为全缓冲printf 写入的数据未填满缓冲区,且进程退出时虽然会刷新,但某些情况下可能不刷新(取决于实现)。解决方法:调用 fflush(stdout)

对比 stderr

c

close(2);
int fd = open("log.txt", ...);
perror("hello world");   // perror 向 stderr 输出

此时 log.txt 立即得到内容,因为 stderr 是无缓冲的。

6. fork 对缓冲区的影响(经典面试题)

c

printf("hello printf\n");
fwrite("hello fwrite\n", 1, 14, stdout);
write(1, "hello write\n", 12);
fork();
  • 直接运行到终端: 三行输出各一次。

  • 重定向到文件: write 输出一次,printf 和 fwrite 各输出两次。

分析:

  • write 是系统调用,无用户态缓冲区,只写入一次。

  • printf 和 fwrite 使用C标准库的缓冲区。重定向到文件时变为全缓冲,数据暂存在用户态缓冲区中,fork 时子进程复制了父进程的缓冲区(写时拷贝),进程退出时父子各自刷新,导致两份数据。

结论: 用户态缓冲区由C标准库提供,位于进程的堆空间中,fork 时会复制。

7. FILE 结构体的内部实现

FILE 是C标准库中管理文件流的结构体,通常包含:

  • 文件描述符 _fileno

  • 缓冲区指针、大小、当前长度

  • 缓冲类型标志

我们可以自己实现一个简化的 my_stdio 来理解原理:

my_stdio.h

c

#define SIZE 1024
#define FLUSH_LINE 1

struct IO_FILE {
    int flag;        // 刷新方式
    int fileno;      // 文件描述符
    char outbuffer[SIZE];
    int cap;
    int size;
};
typedef struct IO_FILE mFILE;

mFILE *mfopen(const char *filename, const char *mode);
int mfwrite(const void *ptr, int num, mFILE *stream);
void mfflush(mFILE *stream);
void mfclose(mFILE *stream);

my_stdio.c(关键部分)

c

mFILE *mfopen(const char *filename, const char *mode) {
    int fd = -1;
    if (strcmp(mode, "w") == 0)
        fd = open(filename, O_CREAT|O_WRONLY|O_TRUNC, 0666);
    // ... 其他模式
    mFILE *mf = malloc(sizeof(mFILE));
    mf->fileno = fd;
    mf->flag = FLUSH_LINE;
    mf->size = 0;
    mf->cap = SIZE;
    return mf;
}

int mfwrite(const void *ptr, int num, mFILE *stream) {
    memcpy(stream->outbuffer + stream->size, ptr, num);
    stream->size += num;
    if (stream->flag == FLUSH_LINE && 
        stream->size > 0 && 
        stream->outbuffer[stream->size-1] == '\n') {
        mfflush(stream);
    }
    return num;
}

void mfflush(mFILE *stream) {
    if (stream->size > 0) {
        write(stream->fileno, stream->outbuffer, stream->size);
        stream->size = 0;
    }
}

通过这个模拟,可以直观理解:FILE 封装了文件描述符和缓冲区,mfwrite 先写缓冲区,遇到换行或手动 mfflush 时才调用 write 系统调用。

8. 内核缓冲区(页缓存)

除了用户态缓冲区,内核也维护了页缓存(Page Cache)。当调用 write 时,数据先拷贝到内核缓冲区,后续由内核线程异步刷盘。这进一步提升了性能。可以使用 fsync 或 sync 强制刷盘。

9. 小结

  • 用户态缓冲区(C标准库提供)减少了系统调用次数,其缓冲类型取决于目标文件是否为终端。

  • 重定向会改变缓冲类型,导致 printf 等行为变化。

  • FILE 结构体封装了文件描述符和缓冲区逻辑。

  • 理解缓冲区是解决输出重定向、fork 后输出重复等问题的关键。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐