Linux 之 【文件】(C语言文件缓冲区)

目录

1.C语言的缓冲区与文件缓冲区

[2.C 语言缓冲区的刷新机制](#2.C 语言缓冲区的刷新机制)

3.C语言缓冲区存在的意义

4.C语言缓冲区存在的位置

5.解释有关现象

6.模拟实现简陋版的C文件标准库

Mystdio.h

Mystdio.c

main.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.解释有关现象

我们先明确一些共识

  1. printf、fprintf、fwrite等函数将数据写入C标准库缓冲区,这些函数不一定立即封装系统调用 ,而是根据缓冲策略在适当时机 (缓冲区满、行缓冲遇到\n、调用fflush()或文件关闭时)才调用 write() 系统调用
  2. 系统调用 write() 和 read() 直接将数据从用户空间传输到内核空间,进入内核的页缓存(所有文件共享的缓冲区)
  3. C语言缓冲区刷新的本质是:将用户空间缓冲区中的数据通过 write() 系统调用复制到内核页缓存。这是一个用户态到内核态的数据拷贝过程
  4. fflush() 通过调用 write() 系统调用,将 C 标准库缓冲区中的数据写入内核,从而完成缓冲区的刷新
  5. 从操作系统视角来看,用户空间包含进程地址空间中的所有内容。因此: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语言文件操作函数的原理了

相关推荐
yuanmenghao2 小时前
车载Linux 系统问题定位方法论与实战系列 - 系统 reset / reboot 问题定位
linux·服务器·数据结构·c++·自动驾驶
n***33352 小时前
Linux命令组合大赛:创意与效率的终极对决
linux·运维·服务器
一个平凡而乐于分享的小比特2 小时前
Linux内核核心组件详解
linux·内存管理·进程间通信·虚拟文件系统·系统调用接口·网络接口
霖霖总总2 小时前
[小技巧30]Linux中getopt 的正确打开方式:原理与实践
linux·运维
不染尘.2 小时前
Linux进程与服务管理
linux·运维·服务器·windows·centos·ssh
Jason_zhao_MR2 小时前
米尔RK3576成功上车!ROS2 Humble生态系统体验
linux·嵌入式硬件·物联网·ubuntu·嵌入式
Cloud Traveler2 小时前
告别餐桌选择困难,YunYouJun cook+cpolar 让私房菜谱走到哪用到哪
linux·运维·自动化
wdfk_prog11 小时前
[Linux]学习笔记系列 -- hashtable
linux·笔记·学习
CheungChunChiu11 小时前
Linux 内核动态打印机制详解
android·linux·服务器·前端·ubuntu