Linux——用户级缓存区及模拟实现fopen、fweite、fclose

发布于:2024-11-28 ⋅ 阅读:(36) ⋅ 点赞:(0)

linux基础io重定向-CSDN博客


文章目录

目录

文章目录

什么是缓冲区

为什么要有缓冲区

二、编写自己的fopen、fwrite、fclose

1.引入函数

2、引入FILE

3.模拟封装

1、fopen

2、fwrite

3、fclose

4、fflush

总结


前言

用快递站讲述缓冲区

收件区(类比输入缓冲区):

快递站的收件区就如同计算机的输入缓冲区。当快递员不断送来包裹(好比计算机从外部设备接收数据),收件区先把这些包裹暂存起来。就像计算机通过输入设备(如键盘输入字符、网络传来数据等)接收到的数据,不会立刻被全部处理,而是先放在输入缓冲区。例如,很多快递在一天内不同时段被送来,收件区堆积着这些待进一步处理的包裹,这类似输入缓冲区存放着源源不断接收到的数据,等待后续的处理步骤。

分拣区(类比处理缓冲区):

这里可比作计算机的处理缓冲区。在快递站分拣区,工作人员会对包裹进行查看、分类(依据目的地等信息),这类似计算机处理缓冲区对暂存的数据进行分析、判断处理方式。比如要确定数据该送往哪个程序或存储区域进行下一步操作,就如同分拣区确定包裹该发往哪个派送点。而且有时候包裹会在分拣区排队等待进一步转运安排,这也如同数据在处理缓冲区可能会因处理资源不足等原因排队等待处理,以确保按正确顺序完成处理。

发件区(类比输出缓冲区):

快递站的发件区就像计算机的输出缓冲区。分拣好准备发往各个派送点的包裹会在发件区暂存(如同计算机处理好的数据在输出缓冲区暂存),等待快递车辆来运走(类似等待合适时机输出到外部设备)。比如一批要发往同一区域的包裹先在发件区集合,就如同一组要输出到显示器显示的数据先在输出缓冲区等待统一发送以便正确显示。同时,发件区工作人员要协调快递车辆到来时间等,保证包裹有序发出,这类似输出缓冲区要协调与外部设备的连接及数据传输时机,确保数据准确输出到目标设备。

总之,快递站的不同区域通过暂存、处理、再暂存并协调输出的流程,很好地模拟了计算机缓冲区在接收、处理、输出数据过程中的作用。


一、缓冲区是什么?

缓冲区是计算机存储体系中的一个特定区域,主要起到暂存数据的作用。

在输入方面:
当外部设备(如键盘、鼠标、网络等)向计算机输入数据时,数据不会立刻被计算机核心部件(如CPU)处理,而是先进入输入缓冲区暂存。比如你通过键盘快速打字,字符数据会先堆积在输入缓冲区,等待后续按合适的速度和顺序被处理,避免因输入速度过快而导致数据丢失或处理混乱。

在输出方面:
当计算机内部处理好的数据要输出到外部设备(如显示器、打印机等)时,也会先暂存到输出缓冲区。例如计算机要在显示器上显示一幅图像,处理好的图像数据会先放在输出缓冲区,然后再按照显示器的刷新频率等要求,适时且有序地将数据传输到显示器进行显示,这样能保证输出的稳定性和连贯性。

在数据处理过程中:
有时数据在不同部件(如CPU和内存之间、不同程序之间等)流转时,也会在中间设置缓冲区来暂存数据,起到协调数据传输速度差异、避免数据冲突等作用。

总之,缓冲区就像是数据流转过程中的一个个“临时仓库”,让数据的输入、输出和处理都能更顺畅、有序地进行。

什么是缓冲区

缓冲区本质上一块内存区域,用来保存临时数据。缓冲区在各种计算任务中都广泛应用,包括输入/输出操作、网络通信、图像处理、音频处理等。

这块内存区域是由谁提供的呢,缓冲区在哪里呢?可以继续向下看.

这里先告诉答案,是C标准库提供的.

为什么要有缓冲区

缓冲区用于解决数据传输速度不匹配或不稳定的问题,并提高数据处理的效率。

当从硬盘读取大量数据时,将数据直接传输到内存中可能会导致读写速度不匹配(内存速度快,而硬盘读取速度慢,这是相对来说的),从而导致性能瓶颈。为了缓解这个问题,可以引入一个缓冲区,先将一部分数据读取到缓冲区中,然后再从缓冲区逐步读取数据到内存中,以平衡数据传输速度。

这里有个很合适的例子来解释:

例如你和你的朋友在两个不同的大学,相差大概500公里,有一天你想送一些书给你的朋友,此时你可以选择骑自行车,亲自骑行去送这些书,礼轻情意重嘛,加上中途休息,然后由于速度慢,花了大概一周的时间才到,送了之后然后又骑回自己的学校,又花了一周的时间,一共过了两周完整的工作才完成,耗时太长。

假设此时你学聪明了,既然那么慢,那么直接坐高铁去送,可来回一共都500多了,这都比这些书的价值多了,即成本太高了.

可以把以上这些书看做资源,这种模式叫做写透模式.

此时你想到,可以寄快递来送这些书啊,价格便宜,而且两三天就到了,这多实惠,于是你把这些书交给了顺丰 快递,过了两三天,你的朋友在手机上给你说,说我收到这些书了,然后这样就成功的把资源交到了对方的手中。这个顺丰快递在这里扮演的角色便是缓冲区. 

顺丰 拿到你的快递也不是立马就送,而是等待数量足够多时,再一次性开始运输,这相当于是一种缓冲区的刷新策略.

缓冲区刷新策略
刷新策略主要有以下3种:

1.立即刷新

2.行刷新(行缓冲),遇到\n刷新

3.满刷新(全缓冲),指的是将输入或输出的数据完全存储在缓冲区中,然后再进行传输或处理。

当然也会有一些特殊情况:

1.用户强制刷新(fflush)

2.进程退出

遇到以上两种情况时,必须马上从刷新缓冲区的数据,而不要按照之前的刷新策略继续等待.

所以缓冲策略 = 一般情况 + 特殊情况.

一般而言,行缓冲的设备文件 --- 显示器

全缓冲的设备文件 --- 磁盘文件

但所有的设备,永远倾向于全缓冲 --> 缓冲区满了再刷新 --> 需要更少次数的IO操作 -->更少次数的外设访问(相当于提高了整机效率).

有同学可能有疑问,比如10行数据,每一行有100个字节,虽然10行最后再一起刷新,只进行了一次的外设访问,但是数据量很多啊,1000个字节,而按行刷新虽然刷新了10次,但每次数据量少啊,那为什么外设访问次数越少越好呢?

这是因为和外部设备IO的时候,数据量的大小不是主要矛盾,你和外设预备IO的过程是最耗费时间的.

比如你和别人借钱,往往沟通的过程要耗费很长时间,而转账的过程只需要几秒,这同样的道理.

那我们直接改成全缓冲不就行了吗?这样效率不就高了吗,还要什么行缓冲.

其实这些策略,都是根据实际情况做的妥协:

例如行缓冲就是针对于显示器,是给用户看的,一方面要照顾效率,另一方面也要照顾用户体验.

而平常我们打开的一些文本文件便是全缓冲,等到用户全部写完再一次性进行保存.

有了这些缓冲区和策略,便可以提高数据处理的效率. 

二、编写自己的fopen、fwrite、fclose

1.引入函数

这些函数都是3好手册的函数,也就是说这些函数可以有2好手册的系统函数来封装。

2、引入FILE

在之前的语言学习时,我们知道“->"引用通常都是结构体,那么stdin、stdout、stderr这三个流也就是结构体了。

因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访 问的。

所以C库当中的FILE结构体内部,必定封装了fd。

man 3 stdin,我们可以看到他们的类型。

​编辑

那么FILE又是什么类型呢?

打开头文件找到EILE类

3.模拟封装

我们想要实现一个文件的打开,写入和关闭文件操作的函数。

#include"myfile.h"
#define myfile "test.txt" 
int main()
{
    //打开文件
    _FILE* _fd= _fopen(myfile,"a");
    if(_fd==NULL)return 1;
    //读文件
    const char*ptr="hello linux!!\n";
    //size_t _fwrite(ptr,strlen(ptr),_fd);
    

    int cnt = 10;
    while(cnt){
        _fwrite(ptr, strlen(ptr),_fd);
        // fflush(fp);
        sleep(1);
        cnt--;
    }
    //写关闭文件
    _fclose(_fd); 


    return 0;
}

我们现在就来实现这个几个函数内部封装。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>


#include <string.h>

#define SIZE 1024
#define FILE_MODE 0666
#define FLUSH_NOW 1
#define FLUSH_LINE 2
#define FLUSH_ALL 4

typedef struct _FILE_IO 
{
    int fileno;
    int flag; 
    //char inbuffer[SIZE];
    //int in_pos;
    char outbuffer[SIZE]; // 用一下这个
    int out_pos;
}_FILE;


_FILE* _fopen(const char*path,const char* made);
size_t _fwrite(const char*ptr,size_t size,_FILE* _stream);
int _fclose(_FILE*_stream);

1、fopen

_FILE* _fopen(const char*path,const char* made)
{
    assert(path);
    assert(made);
    int fd=-1;
    int f=0;
       if(strcmp(made, "w") == 0) {
        f = (O_CREAT|O_WRONLY|O_TRUNC);
        fd = open(path, f, FILE_MODE);
    }
    else if(strcmp(made, "a") == 0) {
        f = (O_CREAT|O_WRONLY|O_APPEND);
        fd = open(path, f, FILE_MODE);
    }
    else if(strcmp(made, "r") == 0) {
        f = O_RDONLY;
        fd = open(path, 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;
}

2、fwrite

size_t _fwrite(const char*ptr,size_t size,_FILE* _stream)
{
     memcpy(&_stream->outbuffer[_stream->out_pos], ptr, size); // 没有做异常处理, 也不考虑局部问题
    _stream->out_pos += size;

    if(_stream->flag&FLUSH_NOW)
    {
        write(_stream->fileno, _stream->outbuffer, _stream->out_pos);
        _stream->out_pos = 0;
    }
    else if(_stream->flag&FLUSH_LINE)
    {
        if(_stream->outbuffer[_stream->out_pos-1] == '\n'){ // 不考虑其他情况
            write(_stream->fileno, _stream->outbuffer, _stream->out_pos);
            _stream->out_pos = 0;
        }
    }
    else if(_stream->flag & FLUSH_ALL)
    {
        if(_stream->out_pos == SIZE){
            write(_stream->fileno, _stream->outbuffer, _stream->out_pos);
            _stream->out_pos = 0;
        }
    }

    return size;
}

3、fclose

int _fclose(_FILE*_stream)
{
    if(_stream == NULL) return -1;
    int fd=_stream->fileno;
    _fflush(_stream);
    close(fd);
    free(_stream);
    return fd;

}

4、fflush

void _fflush(_FILE *fp)
{
    if(fp->out_pos > 0){
        write(fp->fileno, fp->outbuffer, fp->out_pos);
        fp->out_pos = 0;
    }
}

完整代码


总结

缓冲区位于用户程序和硬件设备之间,用来缓存数据,目的是让快速的 CPU 不必等待慢速的输入输出设备,同时减少操作硬件的次数。对于 IO 密集型的网络应用程序(如网站、数据库、DNS、CDN 等),缓冲区的设计至关重要,它能十倍甚至一百倍得提高程序性能。
关于缓冲区还有更多的内容,将在 “文件操作” 一章中深入讲解