Linux->模拟实现 fopen/fread/fwrite

发布于:2025-07-29 ⋅ 阅读:(34) ⋅ 点赞:(0)

目录

引入: 

一:模拟C接口

1:my_fwrite函数

2:my_fclose函数

3:my_fopen函数

4:测试代码

二:模拟缓冲区

1:my_fflush函数

2:my_fwrite函数

3:my_fclose函数

4:my_fopen函数

三:代码测试

四:代码

1:makefile

2:mystdio.h

3:mystdio.c


引入: 

 Linux->基础IO-CSDN博客

我们在上文已经了解到了本文所需的知识,所以不再赘述

所以我们现在模拟实现一下,也就是用系统的调用接口open write  close 去封装出C语言的fopen fwrite  fclose,然后再模拟出一个C缓冲区,去提高效率!

一:模拟C接口

我们先完成用系统调用接口封装C接口

所以我们要先声明一下自己的实现的三个函数,以及file结构体:

#pragma once

#include <stdio.h>


//我模拟实现的FILE结构体
typedef struct _myFILE
{
    int fileno;//存储文件描述符

}myFILE;


//我模拟实现的fopen()
myFILE *my_fopen(const char *pathname, const char *mode);
//我模拟实现的fwrite()
int my_fwrite(myFILE *fp, const char *s, int size);
//我模拟实现的fclose()
void my_fclose(myFILE *fp);

解释:

原本内核中的struct file结构体是有很多的变量的,但是我们模拟实现,只给一个存储fd的变量 fileno即可;那么三个函数的声明中的参数fp,类型当然就是我们自己实现的myFILE*了!

1:my_fwrite函数

int my_fwrite(myFILE *fp, const char *s, int size)
{
    return write(fp->fileno,s,size);
}

解释:

C接口fwrite的本质就是调用系统接口write,write需要的参数fd,直接去结构体中的fileno中拿即可,其次后面两个参数完全吻合,都是写入s中size大小进入文件;

由于目前还没有实现缓冲区,所以我们的my_fwrite函数就直接调用了write函数,如果有缓冲区的话,应该先存储在缓冲区,等到需要刷新的时候,才会调用write函数,所以后面会完善!

2:my_fclose函数

void my_fclose(myFILE *fp)
{
    close(fp->fileno);
    free(fp);
}

解释:my_fclose函数本质就是调用系统接口close,所以很简单。

但是如果有了缓冲区的概念后,my_fclose函数j就需要执行fflush函数了,因为当文件关闭的时候,会将文件的缓冲区的内容强制刷新出来,本质就是my_fclose函数内部显式的调用了fflush函数,所以后面会万神

3:my_fopen函数

myFILE *my_fopen(const char *pathname, const char *mode)
{
    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
    {
        return NULL;
    }

    int fd = 0;
    if(flag & O_WRONLY)
    {
        umask(0);
        fd = open(pathname, flag, 0666);
    }
    else
    {
        fd = open(pathname, flag);
    }
    if(fd < 0) return NULL;

    myFILE *fp = (myFILE*)malloc(sizeof(myFILE));
    if(fp == NULL) return NULL;
    fp->fileno = fd;

    return fp;
}

解释:

my_fopen是唯一复杂的函数,因为其需要把用C接口输入的参数"w" "r" "a"等,转换为系统接口的参数(宏),所以我们输入"r"的时候,我们要将flag |= O_RDONLY,因为flag是0,所以其进行按位或等操作后,会保留后面宏的1,这就代表了其具有了这项功能,比如或等上O_RDONLY,代表其只能读这个文件;后面的也是类似的道理,不再赘述!

当我们完成了对用户输入权限的转换后,才是我们就要根据权限去判断是否需要创建文件了,因为如果用户是"r"权限,则一定不会创建文件,而如果是"w"和"a"权限,则有可能会创建文件!

所以我们总结出了如果flag含有O_WRONLY则一定是"w"和"a"权限中的一种,则我们调用open函数的时候,就给第三个参数传权限,不必担心,如果文件已经存在了,第三个权限参数会不会对其产生影响?是不会的,会被忽略!而当flag不含有O_WRONLY,则其肯定是只读的,所以我们调用open的时候,不传递第三个参数!

当然,最最重要的是,我们open调用完成后,会得到一个文件描述符fd,我们要创建一个myFILE结构体b并将fd填入到myFILE结构体的变量中!因为正是有这个变量,我们的C接口才能找得到文件,还是那句话:

系统调用能直接获取fd,而C接口要通过FILE*去获取fd罢了!

4:测试代码

例子:

在当前路径下对一个不存在的log.txt文件写入5次字符串 "hello wtt!\n"

代码:

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

const char *filename = "./log.txt";

int main()
{
    myFILE *fp = my_fopen(filename, "w");

    char *arr = "hello wtt!\n";
    int cnt = 5;
    while (cnt--)
    {
        my_fwrite(fp, arr, strlen(arr));
    }
    my_fclose(fp);
    return 0;
}

效果如下:

解释:我们原来是不存在这个文件的,其完成了新建,然后往其写入了字符串!符合预期!

Q1:为什么我们写的接口能达到和C接口一样的效果?

A1:因为不论是我们实现的接口还是C的fopen fwrite,本质都只是一个壳子,其内部调用了都是系统接口open write,所以起到效果是必然的,因为什么都是系统调用接口在做,我们实现的只是壳!

Q2:为什么路径要写作"./log.txt",写成"log.txt" ,也可以吗?

const char *filename = "log.txt";

A2:写成"log.txt",效果是一致的!所以我们之前在上篇博客中提到的,我们只写一个文件名字,其会去proc目录下找到进程pid为名的目录,在目录中找到cwd进行拼接!所以现在我们知道了,找到cwd这个动作也是由系统调用接口open完成的,而不是C接口fopen!因为我们实现的my_fopen也可以完成找到cwd功能,但是我们实现的时候,根本没做这件事情,所以侧面体现出了:

找到cwd这个动作也是由系统调用接口open!

 

二:模拟缓冲区

模拟实现的是C缓冲区,不是Linux缓冲区,后者我们现在也没那个实力去实现~~

缓冲区在上篇博客中讲过,其是位于struct file结构体中的,所以我们实现缓冲区,肯定是要放在我们的myFILE结构体中!

所以myFILE结构体以及宏的增加如下:

#pragma once

#include <stdio.h>

#define SIZE 4096 //缓冲区的大小
#define NONE_FLUSH (1<<1) //无刷新
#define LINE_FLUSH (1<<2) //行刷新
#define FULL_FLUSH (1<<3) //全刷新

typedef struct _myFILE
{
    char outbuffer[SIZE];//缓冲区
    int pos;//缓冲区最后字符位置
    int cap;//缓冲区的大小
    int fileno;//存储fd
    int flush_mode;//刷新模式
}myFILE;

解释:

三种刷新策略,定义为宏;其次在结构体中定义了一个缓冲区,值得注意的是,C接口的缓冲区是浮动大小的,这种即保证高效率的同时,又不会浪费空间,但是我们就不那么做了,我们定义为定长的,方便编码!

Q:为什么要有缓冲区?

A:为了高效第一大点中,调用几次my_fwite就会调用多少次write,频繁调用write是非常低效的,因为有时候,我们根本不急着去看文件的内容,你只需要保证我打开文件的时候,内容出现了即可!

1:my_fflush函数

很显然,定义了缓冲区,我们先不用急着去完善其他函数,应该先把my_fflush函数实现了!

因为在我们使用C的时候,除了我们需要显式的调用fflush的时候,其实在某些时候,fflush会被隐式的调用!

①:调用fclose函数的时候,其内部会调用fflush函数,这是必然的,因为你文件都要关闭了,你此时还不把缓冲区的内容刷新到文件中的话,就没有机会了!

②:进程退出的时候,此时C标准库会为我们调用一次fflush,把缓冲区的内容刷新到文件中!

所以我们有必要先实现出my_fflush函数!

代码如下:

void my_fflush(myFILE *fp)
{
    //判断缓冲区是否有内容
    if(fp->pos == 0) return;

    write(fp->fileno, fp->outbuffer, fp->pos);

    //将pos置为0
    fp->pos = 0;
}

解释:刷新,听起来非常高级,其实就是显式的调用一次write函数罢了!!不过我们要先判断缓冲区是否有内容,以及刷新后我们要把pos置为0,因为pos代表缓冲区最后一个字符的位置!

注:fflush不管是在main中被用户直接调用,还是在my_fclose中被调用,其都会接收到一个参数,myFILE*类型的,前者是用户直接把main中my_fopen的返回值传递给它,后者是间接性的把my_fclose的参数间接传给它罢了!

2:my_fwrite函数

所以我们my_fwrite的内部,现在不用也不能直接的区调用write接口了,只会降低效率,应该是先把内容放进缓冲区,再去判断是否需要刷新!所以,不管要不要刷新,内容都是先被放进了缓冲区

代码如下:

int my_fwrite(myFILE *fp, const char *s, int size)
{
    // 1. 写入
    memcpy(fp->outbuffer+fp->pos, s, size);
    fp->pos += size;

    // 2. 判断,是否要刷新

    //行刷新 且 遇到了'\n'?
    if((fp->flush_mode & LINE_FLUSH) && fp->outbuffer[fp->pos-1] == '\n')
    {
        my_fflush(fp);
    }

    //全刷新 且 缓冲区装满了?
    else if((fp->flush_mode & FULL_FLUSH) && fp->pos  == fp->cap)
    {
        my_fflush(fp);
    }
    return size;
}

解释:先写进缓冲区,再判断是否需要刷新,需要则刷新!刷新分为两种:

①:行刷新 且 遇到了'\n'

②:全刷新 且 缓冲区装满了

3:my_fclose函数

void my_fclose(myFILE *fp)
{
    my_fflush(fp);
    close(fp->fileno);
    free(fp);
}

解释:因为有了缓冲区,所以当文件关闭的时候,务必要把缓冲区的内容刷新到文件中!

4:my_fopen函数

myFILE *my_fopen(const char *pathname, const char *mode)
{
    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
    {
        return NULL;
    }

    int fd = 0;
    if(flag & O_WRONLY)
    {
        umask(0);
        fd = open(pathname, flag, 0666);
    }
    else
    {
        fd = open(pathname, flag);
    }
    if(fd < 0) return NULL;

    myFILE *fp = (myFILE*)malloc(sizeof(myFILE));
    if(fp == NULL) return NULL;

    //初始化新文件的myFILE结构体的变量
    fp->fileno = fd;
    fp->cap = SIZE;
    fp->pos = 0;
    fp->flush_mode = FULL_FLUSH;//因为是向文件写入,所以刷新策略为全刷新

    return fp;
}

解释:

唯一多的地方就是对结构体内缓冲区相关变量的初始化,以及确定刷新策略,因为我们是向文件中写入,所以刷新策略采用全刷新!

但是我们的代码有一个无法比拟原生代码的地方,那就是原本的C语言,能够根据我们是向显示器写入还是文件写入,从而动态的选择刷新策略,而我们只能手动更换刷新策略,更抽象的是,我们本来就只能向文件中写入,所以刷新策略锁死了为全刷新!

Q:为什么我们模拟的接口只能向文件中写入,我在my_fwrite的第一个参数传stdout不就好了?

A:首先my_fwrite的第一个参数类型是myFILE*,而不是官方的FILE*,所以根本无法识别stdout!其次我们的刷新策略只在my_fopen函数中malloc文件后才对其初始化,所以一定是全刷新!

三:代码测试

所以我们现在对一个全刷新做测试,其在进程退出之前,我们的文件不会有任何内容,除非我们显式的调用my_fflush或者等待进程退出!

例子1:每两秒向缓冲区写入内容,查看缓冲区的内容,以及缓冲区涉及到的变量的值:

因为缓冲区为4096,想展示将其写满,过于麻烦,所以采取观察缓冲区大小,所以添加两个接口

const char *toString(int flag)
{
    if(flag & NONE_FLUSH) return "None";
    else if(flag & LINE_FLUSH) return "Line";
    else if(flag & FULL_FLUSH) return "FULL";
    return "Unknow";
}

void DebugPrint(myFILE *fp)
{
    printf("outbufer: %s\n", fp->outbuffer);
    printf("fd: %d\n", fp->fileno);
    printf("pos: %d\n", fp->pos);
    printf("cap: %d\n", fp->cap);
    printf("flush_mode: %s\n", toString(fp->flush_mode));
}

解释:方便观察缓冲区的内容,以及缓冲区涉及到的变量的值

测试代码如下:

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

const char *filename = "./log.txt";

int main()
{
    myFILE *fp = my_fopen(filename, "w");
    if(fp == NULL) return 1;

    int cnt = 5;
    char buffer[64];
    while(cnt)
    {
        snprintf(buffer, sizeof(buffer), "helloworld,hellobit,%d  ", cnt--);
        my_fwrite(fp, buffer, strlen(buffer));
        DebugPrint(fp);
        sleep(2);
    }

    my_fclose(fp);
    return 0;
}

 效果:

解释:缓冲区的内容大小不断地在增大,符合预期

例子2:未写满缓冲区,但手动my_fflush:

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

const char *filename = "./log.txt";

int main()
{
    myFILE *fp = my_fopen(filename, "w");
    if(fp == NULL) return 1;

    int cnt = 5;
    char buffer[64];
    while(cnt)
    {
        snprintf(buffer, sizeof(buffer), "helloworld,hellobit,%d  ", cnt--);
        my_fwrite(fp, buffer, strlen(buffer));
        //DebugPrint(fp);
        my_fflush(fp);
        sleep(2);
    }

    my_fclose(fp);
    return 0;
}

效果:

解释:每次在缓冲区的内容,都被手动刷新到了文件中!

最后想说的是,你也可以把刷新策略改为行刷新,这样我们在main中写入的字符最后一个是'\n',则会被刷新到文件中,但是向文件中写入本来就是采取的全刷新,所以不太妥当,自己试试吧

四:代码

1:makefile

filetest:filetest.c mystdio.c
	gcc -o $@ $^
.PHONY:clean
clean:
	rm -f filetest

2:mystdio.h

#pragma once

#include <stdio.h>

#define SIZE 4096
#define NONE_FLUSH (1<<1) //无刷新
#define LINE_FLUSH (1<<2) //行刷新
#define FULL_FLUSH (1<<3) //全刷新

typedef struct _myFILE
{
    char outbuffer[SIZE];//缓冲区
    int pos;//缓冲区最后字符位置
    int cap;//缓冲区的大小
    int fileno;//存储fd
    int flush_mode;//刷新模式
}myFILE;


myFILE *my_fopen(const char *pathname, const char *mode);
int my_fwrite(myFILE *fp, const char *s, int size);
void my_fclose(myFILE *fp);

void my_fflush(myFILE *fp);

void DebugPrint(myFILE *fp);

3:mystdio.c

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

const char *toString(int flag)
{
    if(flag & NONE_FLUSH) return "None";
    else if(flag & LINE_FLUSH) return "Line";
    else if(flag & FULL_FLUSH) return "FULL";
    return "Unknow";
}

void DebugPrint(myFILE *fp)
{
    printf("outbufer: %s\n", fp->outbuffer);
    printf("fd: %d\n", fp->fileno);
    printf("pos: %d\n", fp->pos);
    printf("cap: %d\n", fp->cap);
    printf("flush_mode: %s\n", toString(fp->flush_mode));
}

myFILE *my_fopen(const char *pathname, const char *mode)
{
    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
    {
        return NULL;
    }

    int fd = 0;
    if(flag & O_WRONLY)
    {
        umask(0);
        fd = open(pathname, flag, 0666);
    }
    else
    {
        fd = open(pathname, flag);
    }
    if(fd < 0) return NULL;

    myFILE *fp = (myFILE*)malloc(sizeof(myFILE));
    if(fp == NULL) return NULL;

    //初始化新文件的myFILE结构体的变量
    fp->fileno = fd;
    fp->cap = SIZE;
    fp->pos = 0;
    fp->flush_mode = FULL_FLUSH;//因为是向文件写入,所以刷新策略为全刷新

    return fp;
}

void my_fflush(myFILE *fp)
{
    if(fp->pos == 0) return;
    write(fp->fileno, fp->outbuffer, fp->pos);
    fp->pos = 0;
}

int my_fwrite(myFILE *fp, const char *s, int size)
{
    // 1. 写入
    memcpy(fp->outbuffer+fp->pos, s, size);
    fp->pos += size;

    // 2. 判断,是否要刷新
    if((fp->flush_mode & LINE_FLUSH) && fp->outbuffer[fp->pos-1] == '\n')
    {
        my_fflush(fp);
    }
    else if((fp->flush_mode & FULL_FLUSH) && fp->pos  == fp->cap)
    {
        my_fflush(fp);
    }
    return size;
}

void my_fclose(myFILE *fp)
{
    my_fflush(fp);
    close(fp->fileno);
    free(fp);
}


网站公告

今日签到

点亮在社区的每一天
去签到