《为啥printf重定向后文件是空的?聊聊C语言的缓冲区和FILE》

9 阅读6分钟

1. 缓冲区

1.1 什么是缓冲区

缓冲区其实就是一段内存空间,打个比方:

假如你要给好朋友送一个礼物,但你们在两个不同的城市,由你亲自送要走一个星期,耗费了大量时间。

于是你将礼物交给了楼下的快递驿站,这样你就不需要亲自去送,转而让快递员将礼物送到朋友手里。

快递驿站就扮演了缓存的角色,而礼物就是数据,快递发出就是刷新缓冲区的数据。

但是快递驿站不会为了发你这一件快递就浪费人力资源,所以他会攒到一定数量再发走。

所以:数据允许在缓冲区中积压,这样一次就可以刷新多次数据,变相的减少了IO的次数。

缓存最大的意义是提高使用缓存的进程的效率,允许进程在单位时间内做更多的工作,也就变相的提高了使用者(用户)的效率。

1.2 缓冲类型

在语言层面来看,决定什么时候发快递(决定使用什么样的刷新策略):

  1. 无缓冲,立即刷新。(直接调⽤系统调⽤,标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显⽰出来)
  2. 有缓冲,行刷新。(一般在显示器中使用)
  3. 有缓冲,缓冲区满则刷新。(普通文件一般采用这种方式)

还有两种特殊情况:

  1. 执行fflush语句。
  2. 进程结束。

下面来看一段代码:

#include <stdio.h>      
#include <string.h>     
#include <sys/types.h>  
#include <sys/stat.h>   
#include <fcntl.h>      
#include <unistd.h>   

int main() {
    // 关闭标准输出(文件描述符1)
    close(1);
    
    // 打开或创建文件"log.txt"
    // O_WRONLY: 只写方式
    // O_CREAT: 如果文件不存在则创建
    // O_TRUNC: 如果文件存在则清空内容
    // 0666: 文件权限(所有用户可读写)
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    
    // 检查文件是否成功打开
    if (fd < 0) {
        perror("open");  // 打印错误信息
        return 0;        // 程序结束
    }
    
    // 打印信息到标准输出(现在指向log.txt文件)
    printf("hello world: %d\n", fd);
    
    // 关闭文件
    close(fd);
    
    return 0;  // 程序正常结束
}

这段代码本意是想使用重定向,让本该打印在显示器上的内容写到“log.txt”文件中,但实际运行后会发现,文件中并无内容。

这是因为代码中将1号文件描述符重定向到磁盘文件后,刷新的方式变为了全缓冲。而写入的内容并没有填满缓冲区,所以就不会将缓冲区的内容刷新到磁盘文件中,那么可以怎么做呢?可以使用fflush强制刷新缓冲区。

#include <stdio.h>      
#include <string.h>     
#include <sys/types.h>  
#include <sys/stat.h>   
#include <fcntl.h>      
#include <unistd.h>   

int main() {
    // 关闭标准输出(文件描述符1)
    close(1);
    
    // 打开或创建文件"log.txt"
    // O_WRONLY: 只写方式
    // O_CREAT: 如果文件不存在则创建
    // O_TRUNC: 如果文件存在则清空内容
    // 0666: 文件权限(所有用户可读写)
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    
    // 检查文件是否成功打开
    if (fd < 0) {
        perror("open");  // 打印错误信息
        return 0;        // 程序结束
    }
    
    // 打印信息到标准输出(现在指向log.txt文件)
    printf("hello world: %d\n", fd);
    // 强制刷新
    fflush(stdout);
    // 关闭文件
    close(fd);
    
    return 0;  // 程序正常结束
}

2. FILE

上面谈完了缓冲区,那么缓冲区到底在哪里呢?先说结论,在FILE中。也就是调用fopen函数的返回值的类型。

在操作系统操作文件的时候只认识文件描述符,而fopen之类的函数封装了系统调用函数open,所以fopen的返回值(FILE结构体)中必定包含了文件描述符。

/* 
 * FILE结构体是C标准库中表示文件流的数据结构
 * 它封装了文件描述符和缓冲区,提供更高效的I/O操作
 * 
 * 当我们使用 fopen() 打开文件时,返回的 FILE* 指针就指向一个FILE结构体
 */
typedef struct _IO_FILE FILE;  // 通常的typedef定义

FILE是一个结构体,而在这个结构体内部会维护一块语言级的缓冲区空间。所以当我们调用printf之类的函数时,是将输出格式化成字符串写入到FILE结构体的缓冲区,再按照刷新规则刷新到输出设备上。下面是FILE结构体内部的一部分:

struct _IO_FILE {
    int _fileno;               // 文件描述符(底层open返回的fd)
    char* _IO_read_ptr;        // 读缓冲区当前位置
    char* _IO_read_end;        // 读缓冲区结束位置
    char* _IO_read_base;       // 读缓冲区起始位置
    char* _IO_write_ptr;       // 写缓冲区当前位置
    char* _IO_write_end;       // 写缓冲区结束位置
    char* _IO_write_base;      // 写缓冲区起始位置
    char* _IO_buf_base;        // 缓冲区起始位置
    char* _IO_buf_end;         // 缓冲区结束位置
    int _flags;                // 文件状态标志
    // ... 还有其他字段
};
  1. 所以在学习例如C语言的时候,所说的缓冲区实际上是语言级缓冲区,和内核没有关系!
  2. 缓冲区在哪?FILE内部!
  3. 为什么要有语言级别缓冲区?因为调用系统调用是有成本的(浪费时间),所以可以减少系统调用次数。

3. 模拟文件操作

下面通过对系统调用封装,模拟实现一下C标准库的文件操作接口。

3.1 my_stdio.h

#ifndef __MYSTDIO_H__
#define __MYSTDIO_H__

#define FLUSH_NONE 1
#define FLUSH_LINE 2
#define FLUSH_FULL 4

#define SIZE 4096
#define UMASK 0666

#define FORCE 1
#define NORMAL 2

typedef struct _MY_IO_FILE
{
    int fileno; //文件描述符
    int flag; //刷新方式
    char outbuffer[SIZE];   //缓冲区
    int curr;   //当前缓冲区字符数量
    int cap;    //当前缓冲区总容量
}MYFILE;

MYFILE* my_fopen(const char* filename, const char* mode);

void my_fclose(MYFILE* fp);

int my_fwrite(const char* s, int size, MYFILE* fp);

void my_fflush(MYFILE* fp);

#endif

3.2 my_stdio.c

#include "my_stdio.h"
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>

// 打开文件
MYFILE *my_fopen(const char *filename, const char *mode)
{
    int fd = -1;
    if (strcmp(mode, "w") == 0)
    {
        fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, UMASK);
    }
    else if (strcmp(mode, "r") == 0)
    {
        fd = open(filename, O_RDONLY);
    }
    else if (strcmp(mode, "a") == 0)
    {
        fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, UMASK);
    }
    else if (strcmp(mode, "a+") == 0)
    {
        fd = open(filename, O_CREAT | O_RDONLY | O_APPEND, UMASK);
    }

    if (fd < 0)
        return NULL;
    MYFILE *fp = (MYFILE *)malloc(sizeof(MYFILE));
    if (fp == NULL)
        return NULL;

    fp->fileno = fd;
    fp->flag = FLUSH_LINE;
    fp->curr = 0;
    fp->cap = SIZE;
    fp->outbuffer[0] = 0;

    return fp;
}

static void my_fflush_core(MYFILE *fp, int force)
{
    if (fp->curr <= 0)
        return;

    if (force == FORCE)
    {
        write(fp->fileno, fp->outbuffer, fp->curr);
        fp->curr = 0;
    }
    else
    {
        // 判断刷新条件
        if ((fp->flag & FLUSH_LINE) && (fp->outbuffer[fp->curr - 1] == '\n'))
        {
            // 行刷新
            write(fp->fileno, fp->outbuffer, fp->curr);
            fp->curr = 0;
        }
        else if ((fp->flag & FLUSH_FULL) && (fp->curr == fp->cap))
        {
            // 满刷新
            write(fp->fileno, fp->outbuffer, fp->curr);
            fp->curr = 0;
        }
        else
        {
            write(fp->fileno, fp->outbuffer, fp->curr);
            fp->curr = 0;
        }
    }
}

int my_fwrite(const char *s, int size, MYFILE *fp)
{
    memcpy(fp->outbuffer + fp->curr, s, size);
    fp->curr += size;
    my_fflush_core(fp, NORMAL);
    return size;
}

void my_fflush(MYFILE *fp)
{
    my_fflush_core(fp, FORCE);
}

void my_fclose(MYFILE *fp)
{
    if (fp->fileno >= 0)
    {
        my_fflush(fp);
        fsync(fp->fileno);
        close(fp->fileno);
        free(fp);
    }
}