基础IO(二)—— 缓冲区的理解/模拟实现C标准库文件操作函数

基础IO(二)

缓冲区

Linux下的文件缓冲区是内存中的一个特定区域,专门用于临时存储文件读取或写入的数据。这种机制旨在优化对磁盘或网络的访问,提高数据传输的效率,并减少系统调用的次数。以下是对Linux下文件缓冲区的详细解析:

定义与功能

  • 定义:文件缓冲区是内存空间的一部分,用于在数据实际写入磁盘或从磁盘读取之前,作为临时存储的中介。
  • 功能
    1. 提高性能:通过减少磁盘I/O操作的次数,文件缓冲区能够显著提高数据读写操作的性能。
    2. 优化资源利用:合理利用内存资源,避免频繁的直接磁盘访问,降低系统开销。
    3. 数据缓存:存储最近访问或即将访问的数据,以便快速响应后续请求。

为了更深刻的理解缓冲区的功能下面举个例子

小明和小王在两个不同的地方,小明想给小王发送一个包裹,但是小明不能直接跑到小王的地方把包裹直接给小王,而是先将包裹给菜鸟驿站,然后菜鸟驿站帮忙给小王。但是菜鸟驿站不能只收到小明的包裹就发送给小王而是收到了好多的包裹之后一起给小王。

在这个过程中,小明就相当于用户而菜鸟驿站就像是一个缓冲区。

在这里插入图片描述

缓冲区主要的还是提高使用者的效率,因为有缓冲区的存在我们可以积累一部分在统一发送,提高发送的效率。

工作原理

  • 当程序尝试读取文件时,操作系统会先将数据从磁盘读取到文件缓冲区中,然后再将数据从缓冲区传输到用户程序。
  • 类似地,当程序写入文件时,数据首先被写入到文件缓冲区中,而不是直接写入磁盘。数据在缓冲区中积累,直到满足一定的条件(如缓冲区满、文件关闭或显式刷新缓冲区)时,才会被实际写入磁盘。

用户级缓冲区和内核级缓冲区

用户级缓冲区缓冲方式

缓冲区因为能够暂存数据,必定有一定的刷新方式。将用户层的数据刷新到内核中。Linux下的文件缓冲方式根据积累的程度不同主要有以下几种:

  • 全缓冲(缓冲区满了,在刷新):在缓冲区填满之后才进行实际的I/O操作,适用于磁盘文件等大规模数据传输。
  • 行缓冲(行刷新):在遇到换行符时刷新缓冲区,常用于标准输入输出(如终端)。
  • 无缓冲(立即刷新):数据直接写入或读取,不进行缓冲,适用于需要即时响应的场景。

除了上述三种刷新策略还存在两种特殊情况

  • 强制刷新
  • 进程退出的时候,一般要进行刷新缓冲区

一般对于显示器文件,进行行刷新,对于磁盘上的文件一般采用全缓冲(缓冲写满,在刷新)

下面一段代码能更深刻的理解刷新方法

代码1

#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
    fprintf(stdout,"C函数:hello fprintf\n");
    printf("C函数:hello printf\n");
    fputs("C函数:hello fputs\n",stdout);

    const char *str = "system call: hello write\n";

    //向标准输出写入
    write(1,str,strlen(str));
    
    return 0;
}

在这里插入图片描述

当我们直接向显示器打印的时候,显示器文件的刷新方式是行刷新,而且你的代码输出的所有字符串,都有‘\n’,fork之前,数据全部已经被刷新,包括系统调用(systemcall

代码2

int main()
{
    fprintf(stdout,"C函数:hello fprintf\n");
    printf("C函数:hello printf\n");
    fputs("C函数:hello fputs\n",stdout);

    const char *str = "system call: hello write\n";

    //向标准输出写入
    write(1,str,strlen(str));
    fork();//创建子进程
    return 0;

}

在这里插入图片描述

理解案例

我们发现在创建子进程后,C语言的接口在文件中存在了两份而系统调用接口只存在了一份。

重定向到log.txt本质是向磁盘文件中写入,系统对于数据的刷新方式已经由行刷新,变成了全缓冲。

全缓冲意味着缓冲区变大,实际写入的简单数据,不足以把缓冲区写满,fork执行的时候,数据依然在缓冲区中。上述代码的缓冲区和操作系统是没有关系的,只和C语言有关系。我们日常用的最多的缓冲区其实就是C/C++提供的语言级别的缓冲区。如果我们把数据交给了OS,这个数据就属于OS,不属于自己的进程了。

当进程退出的时候,一般要进行刷新缓冲区,即使数据没有满足刷新条件,所以当fork之后,任意一个进程退出了,就会发生写实拷贝。但是对于write系统调用,没有使用C的缓冲区,而是直接写入到操作系统,不属于进程了就不会发生写实拷贝

在次理解刷新

从C缓冲区写入OS的工作叫做刷新

图解

在这里插入图片描述

缓冲区在哪里

在FILE结构体中存在一个缓冲区

下面是FILE结构体中的一段代码

struct _IO_FILE {
 int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
 //缓冲区相关
 /* The following pointers correspond to the C++ streambuf protocol. */
 /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
 char* _IO_read_ptr; /* Current read pointer */
 char* _IO_read_end; /* End of get area. */
 char* _IO_read_base; /* Start of putback+get area. */
 char* _IO_write_base; /* Start of put area. */

 char* _IO_write_ptr; /* Current put pointer. */
 char* _IO_write_end; /* End of put area. */
 char* _IO_buf_base; /* Start of reserve area. */
 char* _IO_buf_end; /* End of reserve area. */
 /* The following fields are used to support backing up and undo. */
 char *_IO_save_base; /* Pointer to start of non-current get area. */
 char *_IO_backup_base; /* Pointer to first valid character of backup area */
 char *_IO_save_end; /* Pointer to end of non-current get area. */
 struct _IO_marker *_markers;
 struct _IO_FILE *_chain;
 int _fileno; //封装的文件描述符
#if 0
 int _blksize;
#else
 int _flags2;
#endif
 _IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
 /* 1+column number of pbase(); 0 is unknown. */
 unsigned short _cur_column;
 signed char _vtable_offset;
 char _shortbuf[1];
 /* char* _save_gptr; char* _save_egptr; */
 _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

我们可以看到代码中存在大量的char*类型这些就是指向缓冲区的指针。

内核级缓冲区刷新方式

缓冲区类型

内核缓冲区主要分为两种类型:buffer(缓冲)和cache(缓存)。它们虽然都位于内核空间,但功能和使用场景有所不同。

  • Buffer:主要用于存储尚未写入到磁盘的数据,如文件系统的写操作。当数据积累到一定量后,buffer会将其写入磁盘,从而降低磁盘I/O的频率。
  • Cache:用于暂存来自磁盘的数据,以便快速访问。这可以提高数据重用性,减少系统对磁盘的频繁访问。

刷新时机

内核缓冲区的刷新时机通常基于多种因素,包括但不限于:

  • 缓冲区满:当缓冲区中的数据量达到其容量上限时,系统会自动触发刷新操作,将缓冲区中的数据写入目标设备(如磁盘)。
  • 显式刷新:用户或系统程序可以通过调用特定的系统调用或命令来显式刷新缓冲区,如Linux中的sync命令。
  • 进程退出:当进程结束时,系统通常会检查并刷新所有未刷新的缓冲区,以确保数据的一致性和完整性。
  • 定时刷新:某些系统可能会设置定时器,定期检查并刷新缓冲区中的数据。
  • 基于LRU算法的页面回收Linux等操作系统使用LRU(最近最少使用)算法来管理内存页面,包括缓冲区页面。当系统内存紧张时,会回收长时间未使用的页面,包括可能包含脏数据的缓冲区页面。此时,脏页面中的数据会被写回磁盘。

刷新策略

内核缓冲区的刷新策略通常基于系统的具体实现和配置,但一般遵循以下原则:

  • 优化性能:通过减少磁盘I/O次数和延迟来提高系统性能。
  • 保证数据一致性:确保在适当的时候将缓冲区中的数据刷新到磁盘,以避免数据丢失或不一致。
  • 灵活配置:允许用户或系统管理员根据实际需求调整刷新策略,如设置刷新频率、缓冲区大小等。

缓冲区与内核的关系

  • 用户程序中的缓冲区是用户空间的一部分,而操作系统内核也有自己的缓冲区(内核缓冲区)来管理磁盘I/O操作。
  • 当用户程序将数据写入缓冲区时,这些数据最终会被传输到内核缓冲区,并由内核负责将数据写入磁盘。
  • 内核缓冲区的刷新策略由操作系统自主决定,通常包括基于时间的刷新、基于磁盘空间的刷新等多种机制。

模拟实现C标准库的函数

mystdio.h

#pragma once

#define size 4096


typedef struct _myFILE
{
    int fileno; //文件描述符
    int flag;
    char buffer[size];//文件缓冲区
    int end;//记录文件结尾

}myFILE;

//打开文件
extern myFILE *my_fopen(const char* path, const char* mode);
//向文件中写内容
extern int my_fwrite(const char* s, int num, myFILE* stream);
//刷新文件缓冲区
extern int my_fflush(myFILE *stream);
//关闭文件
extern int my_fclose(myFILE *stream);

mystdio.c

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

#define DFL_MODE 0666

myFILE *my_fopen(const char *path, const char *mode)
{
    int fd   = 0;
    int flag = 0;
    if(strcmp(mode, "r") == 0)
    {
        flag |= O_RDONLY;
    }
    else if(strcmp(mode, "w") == 0)
    {
        flag |= (O_CREAT | O_TRUNC | O_WRONLY);
    }
    else if(strcmp(mode, "a") == 0)
    {
        flag |= (O_CREAT | O_WRONLY | O_APPEND);
    }
    else{
        // Do Nothing
    }
    if(flag & O_CREAT)
    {
        fd = open(path, flag, DFL_MODE);
    }
    else
    {
        fd = open(path, flag);
    }

    if(fd < 0)
    {
        errno = 2;
        return NULL;
    }

    myFILE *fp = (myFILE*)malloc(sizeof(myFILE));
    if(!fp) 
    {
        errno = 3;
        return NULL;
    }
    fp->flag = FLUSH_LINE;
    fp->end = 0;
    fp->fileno = fd;
    return fp;
}
int my_fwrite(const char *s, int num, myFILE *stream)
{
    // 写入
    memcpy(stream->buffer+stream->end, s, num);
    stream->end += num;

    // 判断是否需要刷新, "abcd\nefgh"
    if((stream->flag & FLUSH_LINE) && stream->end > 0 && stream->buffer[stream->end-1] == '\n')
    {
        my_fflush(stream);
    }

    return num;
}
int my_fflush(myFILE *stream)
{
    if(stream->end > 0)
    {
        write(stream->fileno, stream->buffer, stream->end);
        //fsync(stream->fileno);
        stream->end = 0;
    }

    return 0;
}
int my_fclose(myFILE*stream)
{
    my_fflush(stream);
    return close(stream->fileno);
}

main.c

#include "mystdio.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main()
{
    myFILE *fp = my_fopen("./log.txt", "w");
    if(fp == NULL)
    {
        perror("my_fopen");
        return 1;
    }
    int cnt = 20;
    const char *msg = "haha, this is my stdio lib";
    while(cnt--){
        my_fwrite(msg, strlen(msg), fp);
        sleep(1);
    }
    my_fclose(fp);
    return 0;
}

上一篇:基础IO(一)

本专栏为“小菜”linux学习之路
该文章仅供学习参考,如有问题,欢迎在评论区指出。

  • 9
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值