前言
经过我们上一篇对linux系统文件操作的学习想必我们已经会使用系统文件接口了,今天我们就用系统文件接口来封装一个像C语言库那样的文件操作函数的函数来加深我们对文件操作的学习。
一、模拟C库文件操作
首先我们创建相应的.c .h 以及main.c头文件,然后我们写一个makefile:
接下来我们完善.h里面的代码:
#pragma once #include <stdio.h> #define NUM 1024 #define BUFF_NONE 0x1 #define BUFF_LINE 0x2 #define BUFF_ALL 0x4 typedef struct _MY_FILE { int fd; char outputbuffer[NUM]; int flags; //刷新策略 }MY_FILE; MY_FILE* my_fopen(const char* path,const char* mode); size_t my_fwrite(const void* ptr,size_t size,size_t nmemb,MY_FILE* stream); int my_fclose(MY_FILE* fp);
首先有一个文件结构体,在这个结构体中有文件描述符,有一个1024大小的缓冲区,还有控制刷新的标志,我们为了方便将结构体重命名为MY_FILE,既然有刷新策略那么我们就#define3个刷新策略,分别是无缓冲,行缓冲,全缓冲。然后我们实现三个函数,分别是打开文件,写入文件和关闭文件,这些函数我们都是通过系统接口调用所以参数与系统接口一致,下面我们进行函数主体的编写:
#include "mystdio.h" #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <malloc.h> #include <assert.h> #include <unistd.h> MY_FILE* my_fopen(const char* path,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 { } //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; } //给用户返回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 = BUFF_LINE; memset(mf->outputbuffer,'\0',sizeof(mf->outputbuffer)); //my_outputbuffer[0] = 0; //初始化缓冲区 //5.返回打开的文件 return mf; } int my_fflush(MY_FILE* fp) { return 0; } size_t my_fwrite(const void* ptr,size_t size,size_t nmemb,MY_FILE* stream) { return 0; } 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; }
下面我们一个函数一个函数的进行讲解,首先是fopen函数:
MY_FILE* my_fopen(const char* path,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 { } }
对于打开文件的函数我们要判断用户是什么模式下的打开文件,我们设置标志位为0,如果用户输入的方式是r只读模式,就让标志位或上只读选项,如果是w写模式,这个时候要有三种实现,文件不存在就创建文件,只写文件,写之前先清空文件,搞定w模式后我们完成追加模式,对于a来讲也有三种情况,文件不存在就创建文件,只写文件,追加文件不进行清空,搞定第一步后我们完成第二步打开文件:
//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; }
要打开文件我们先创建一个默认的文件权限0666,并且创建一个文件标识符变量,判断文件是否存在,如果存在就用不需要权限参数的open函数代开文件,如果不存在则需要用三个参数的open函数打开文件。如果返回值小于0说明打开文件失败返回空指针。
//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 = BUFF_LINE; memset(mf->outputbuffer,'\0',sizeof(mf->outputbuffer)); //my_outputbuffer[0] = 0; //初始化缓冲区 //5.返回打开的文件 return mf;
第三步给用户返回一个文件对象,先创建一个指针用来开一个MY_FILE的结构体,然后判断是否创建成功如果不成功我们就将文件关闭并且返回NULL,如果成功我们就初始化MY_struct中的参数,将文件标识符传过去,因为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; }
关闭文件的函数很简单,因为关闭文件前需要先刷新缓冲区所以我们在struct中添加一个变量current来判断是否需要刷新缓冲区,冲刷完缓冲区后将文件关闭,最后将malloc出来的struct结构体空间释放掉,为了安全起见可以将fp指针置为NULL。
#include "mystdio.h" #define MYFILE "log.txt" int main() { MY_FILE* fp = my_fopen(MYFILE,"w"); if (fp==NULL) { return 1; } my_fclose(fp); return 0; }
下面我们演示一下我们的文件操作成功与否:
通过上图我们可以看到我们写的文件操作函数是成功的,只是有些功能我们还没有去完善,如果都完善了可以替代C语言库用我们自己的库。
下面我们将fwrite接口补充完整:
//我们今天返回的就是一次实际写入的字节数,我们就不返回个数了 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); 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; }
我们写入的第一件事就是判断缓冲区是否有空间,如果我们的缓冲区已经满了我们就直接把数据刷新出去。如果我们期望写入4096字节,但是缓冲区只有1024字节,所以最后缓冲区只能输出1024字节,所以下一步我们要根据缓冲区剩余情况进行数据拷贝即可。current是当前缓冲区有多少字符,并且current也是下一次缓冲区继续写入的下标。接下来我们计算用户传的缓冲区有多大,再计算我们自己的缓冲区还剩下多少,如果我们的剩余空间是大于用户的数据的,说明我们的空间是足够的,直接把用户的缓冲区的数据拷贝到我们自己的缓冲区即可,在这里要注意不能直接拷贝到stream->outbuffer,因为当前缓冲区很可能是有数据的我们不能覆盖,只能从current的位置继续拷贝,拷贝完成后记得更新我们缓冲区的位置,由于fwrite函数写入成功会返回当前写入多少字节,所以我们用一个变量记录当前写入了多少字节,如果我们当前的空间不足以容纳用户传来的空间,我们就只能拷贝我们剩余大小的空间,然后接下来我们计划开始刷新缓冲区,如果是全缓冲就等下标等于NUM的时候刷新,如果是行刷新我们直接判断current-1的位置是不是\n,如果是\n我们就进行行刷新。最后返回写入了多少字节就完成了,接下来我们完成刷新函数:
int my_fflush(MY_FILE* fp) { assert(fp); write(fp->fd,fp->outputbuffer,fp->current); return 0; }
首先判断fp是否为空指针,如果不是我们就进行刷新,刷新我们直接用write函数,第一个参数是文件描述符,第二个参数是缓冲区,第三个参数是写入多少的大小,总体就是向文件描述符中写入特定的缓冲区中的特定大小,接下来我们在main函数中试试我们的fwrite函数。
#include "mystdio.h" #define MYFILE "log.txt" #include <string.h> #include <unistd.h> int main() { MY_FILE* fp = my_fopen(MYFILE,"w"); if (fp==NULL) { return 1; } const char* str = "hello my fwrite"; int cnt = 5; while (cnt) { char buffer[1024]; snprintf(buffer,sizeof(buffer),"%s:%d\n",str,cnt--); size_t size = my_fwrite(buffer,strlen(buffer),1,fp); sleep(1); printf("当前成功写入:%lu个字节\n",size); } my_fclose(fp); return 0; }
接下来我们运行一下看看效果:
当我们运行后发现,其他的都没问题但是为什么多打印了那么多?因为我们每次打印后都要重新刷新一下缓冲区再进行打印,否则每次都带有之前旧的数据被打印出来,下面我们将缓冲区函数改一下:
int my_fflush(MY_FILE* fp) { assert(fp); write(fp->fd,fp->outputbuffer,fp->current); //刷新后清理缓冲区 fp->current = 0; return 0; }
通过上图我们可以看到这次的程序是成功完成。这就是我们模拟的文件操作,通过这样的实现我们更深刻的理解了系统文件操作。
区又有什么块的知识,下一篇文章我们将详细介绍文件的软硬链接。