【Linux】基础IO——文件描述符:缓冲区的理解

上个月学校考试,进行课程复习,一直没有更新博客,现考试结束,继续保持更新,欢迎大家关注!

1 模仿C库自主封装简单的文件接口

目的:简单理解C语言是如何封装各个文件操作接口的。
1.接口实现:

#include "mystdio.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <malloc.h>
#include <assert.h>

MY_FILE *my_fopen(const char *path, const char *mode)
{
    //1.识别标志位
    int flag = 0;
    if(strcmp(mode, "r") == 0) flag |= O_RDONLY;
    else if(strcmp(mode, "w") == 0) flag |= (O_CREAT | O_WRONLY | O_TRUNC);
    else if(strcmp(mode, "a") == 0) flag |= (O_CREAT | O_WRONLY | O_APPEND);
    else{
        //其他模式
    }

    //2.打开文件
    mode_t m = 0666;
    int fd = 0;
    if(flag & O_CREAT) fd = open(path, flag, m);
    else fd = open(path, flag);
    if(fd < 0) return NULL;

    //3.构建MY_FILE结构体对象
    MY_FILE* mf = (MY_FILE*)malloc(sizeof(MY_FILE));
    if(mf == NULL)
    {
        close(fd);
        return NULL;
    }

    //4.初始化MY_FILE对象
    mf->fd = fd;
    mf->flags = 0;
    mf->flags |= BUFF_LINE;
    memset(mf->outputbuffer, '\0', sizeof(mf->outputbuffer));
    mf->current = 0;

    //5.返回打开的文件(MY_FILE)
    return mf;
}

int my_fflush(MY_FILE *fp)
{
    assert(fp);
    write(fp->fd, fp->outputbuffer, fp->current);
    fp->current = 0;
    return 0;
}

size_t my_fwrite(const void *ptr, size_t size, size_t nmemb, MY_FILE *stream)
{
    //1.缓冲区满了,就直接刷新
    if(stream->current == NUM) my_fflush(stream);

    //2.缓冲区没满,把数据拷贝进缓冲区
    size_t user_size = size * nmemb;
    size_t my_size = NUM - stream->current;

    size_t writen = 0;//实际写入的字节数
    if(my_size >= user_size)
    {
        memcpy(stream->outputbuffer + stream->current, ptr, user_size);
        //3.更新计数器字段
        stream->current += user_size;
        writen = user_size;
    }
    else
    {
        memcpy(stream->outputbuffer + stream->current, ptr, my_size);
        //3.更新计数器字段
        stream->current += my_size;
        writen = my_size;
    }

    //4.开始计划刷新
    if(stream->flags & BUFF_ALL)
    {
        if(stream->current == NUM) my_fflush(stream);
    }
    else if(stream->flags & BUFF_LINE)
    {
        if(stream->outputbuffer[stream->current - 1] == '\n') my_fflush(stream);
    }
    else
    {}

    return writen;
}

//关闭文件的时候,C会帮助我们冲刷缓冲区
int my_fclose(MY_FILE *fp)
{
    assert(fp);
    //1.冲刷缓冲区
    if(fp->current > 0) my_fflush(fp);
    //2.关闭文件
    close(fp->fd);
    //3.释放堆空间
    free(fp);
    //4.指针NULL
    fp = NULL;

    return 0;
}

2.测试代码:

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

#define MYFILE "log.txt"

int main()
{
    MY_FILE *fp = my_fopen(MYFILE, "w");
    if(fp == NULL) return 1;
    const char *str = "hello my_write";
    int cnt = 500;
    while(cnt)
    {
        char buffer[1024];
        //snprintf(buffer, sizeof(buffer), "%s: %d\n", str, cnt--);
        snprintf(buffer, sizeof(buffer), "%s: %d", str, cnt--);
        size_t size = my_fwrite(buffer, strlen(buffer), 1, fp);
        sleep(1);
        printf("当前成功写入:%lu个字节\n", size);

        if(cnt % 5 == 0) 
        {
            my_fwrite("\n", strlen("\n"), 1, fp);
        }

    }

    my_fclose(fp);
    return 0;
}

注:测试代码分为两种情况:

  1. 写入数据时不添加\n,写5次手动在缓冲区写入一个\n,来完成一次行刷新。
  2. 写入数据时添加\n,写一次刷新一次数据。

观察运行结果,思考:
写入时存在缓冲区,先将数据写入到缓冲区中,然后再刷新到显示器,那为什么不直接将数据刷新到显示器呢?为什么要有缓冲区?
——因为可以先将数据存在缓冲区中,不发生刷新,不进行写入,也就是不进行IO,不进行系统调用,所以函数会调用的很快,数据会暂存在缓冲区中。在缓冲区中可以积压多份数据,最后统一刷新。
其本质就是一次IO可以IO多份数据,从而提高IO的的效率。

2 对缓冲区的理解

以前我们所说的的缓冲区,是指用户级别的缓冲区,由语言提供。
下面要理解一个完整的缓冲区——用户层+内核,进而引入强制刷新内核

2.1 数据刷新到磁盘的过程分析

我们要把数据保存到磁盘文件中,至少要进行3次拷贝,下面进行解析:
当我们每次访问一个文件的时候,其实就是通过进程在访问文件,所以进程拥有对应的tesk_struct,拥有对应的文件描述符表struct file* fd_array[](在files_struct中),拥有被打开的文件struct file,并且被打开的文件拥有自己的文件缓冲区。
并且我们平时使用的C语言,存在C标准库libc.a/so,库当中为我们提供了对应的方法(比如fwrite/fput等),并且提供的这些所有的接口,都拥有FILE*参数,这个FILE结构体对象是在我们使用open打开文件的时候库在内部为我们创建好的,我们可以理解成在库里面或者在自己的进程中(我们理解为放在库中),FILE中存在一个缓冲区,所以我们在使用库提供的接口(fwrite/fput等)时,其实是先将数据存放在这个缓冲区中,所以我们使用的比如fwrite/fput这些接口其实本质是拷贝函数,它们将我们的数据拷贝到了FILE结构体的缓冲区中,然后再结合函数的刷新策略(通过打开文件的类型以及缓冲区是否满了判断),结合OS提供的比如write系统调用接口(write中有文件描述符fd、要写入的文件缓冲区的地址),定期的将数据冲刷到文件缓冲区(前面说的被打开的文件拥有自己的缓冲区)中,所以系统调用接口write的本质也是拷贝函数

那么C库提供的 “拷贝函数” 与系统调用接口 “拷贝函数” 有什么不同呢?

  • C库提供的拷贝函数是从用户——>语言
  • 系统调用拷贝函数是从语言——>内核

当数据到达文件缓冲区,OS就要有自己的刷新策略(所以刷新策略不止全缓冲、行缓冲、无缓冲)!OS可以将数据暂存在这个缓冲区中,以时间为单位刷新到磁盘;也可以等缓冲区满了再冲刷到磁盘;也可以判断当内存使用很紧张的时候,将数据刷新到磁盘中;也可以为了提高刷新速度,积累一定量的数据后再一次刷新。(我们平时写word,ctrl+s保存就是手动将保存在缓冲区中的数据刷新到磁盘,方式断电缓冲区中的数据丢失)
实际上OS的刷新策略要比上层用户的刷新策略复杂得多,因为OS要综合考虑的因素很多。但是总体的思想还是一样的——通过一次IO刷新更多的数据来提升刷新速度。

注:OS的刷新策略我们是不可见的。

总结: 要将数据刷新到硬件上至少要有三次拷贝:

  1. 第一次拷贝:先将数据刷新到语言库的缓冲区上
  2. 第二次拷贝:通过系统调用将数据拷贝到对应的内核当中
  3. 第三次拷贝:将数据从内存拷贝到外设

上面所讲的刷新过程图解:
在这里插入图片描述

2.2 如何强制刷新内核

例如我们在使用word的时候,ctrl+s强制刷新到外设,防止数据丢失。
所以系统也要提供接口供用户自主将数据刷新到外设。

系统提供的接口:

功能:通过文件描述符fd,将缓冲区数据刷新到外设。
接口:int fsync(int fd);

所以在实现fflush的时候,调用fsync来将数据刷新到外设:

int my_fflush(MY_FILE *fp)
{
    assert(fp);
    write(fp->fd, fp->outputbuffer, fp->current);
    fp->current = 0;

    fsync(fp->fd);
    return 0;
}

补充理解:
综上理解,当我们调用printf打印例如1245这样的数字时,其实我们打印的是字符1 2 3 4 5,即打印的是字符串,只是这些字符是连在一起的,在我们看来打印的是一串数字。但是在C程序中,12345是一个整数,但是显示的时候却打印成了字符串,所以肯定出现了一次数据格式转化,数据格式转化是printf做的(所以printf叫格式控制),它将内存的数据转换成了字符串数据。

那么printf是如何进行格式控制的呢?

  1. 先获取对应的变量x
  2. 定义缓冲区,将x转换成字符串
  3. 将字符串拷贝到stdout->buffer
  4. 集合具体的刷新策略刷新显示即可
int my_printf(const char *format, ...)
{
	//1. 先获取对应的变量x
    //2. 定义缓冲区,将x转换成字符串
	//3. 将字符串拷贝到stdout->buffer
	//4. 集合具体的刷新策略刷新显示即可
}

再比如scanf是如何进行格式控制的呢?

  1. 首先获取数据读取到stdin->buffer中
  2. 对buffer的内容进行格式化,写入到对应的变量当中

写一段伪代码进行解释:
int a, b;
scanf(“%d %d”, &a, &b); //假设输入123 456
其本质就是:

  1. 将数据读取到缓冲区中:read(0, stdin->buffer, num);
  2. 本质输入的是123 456 字符串
  3. 扫描字符串,碰到空格,字符串就被分为2个子串
  4. 分别对两个字串进行格式转换后写入即可:*ap = atoi(str1); *bp = atoi(str2);

总结:
我们在学习比如C、C++等语言的时候,所说的缓冲区概念都是由该语言提供的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值