目录
引入:
我们在上文已经了解到了本文所需的知识,所以不再赘述
所以我们现在模拟实现一下,也就是用系统的调用接口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);
}