河南省城乡住房建设厅网站,建设银行车贷网站,网站建设 天津,网站建设及相关流程✨个人主页#xff1a; 北 海 #x1f389;所属专栏#xff1a; Linux学习之旅 #x1f383;操作环境#xff1a; CentOS 7.6 阿里云远程服务器 文章目录 #x1f307;前言#x1f3d9;️正文1、FILE 结构设计2、函数使用及分析3、文件打开 fopen4、文件关闭 fclose5、缓… ✨个人主页 北 海 所属专栏 Linux学习之旅 操作环境 CentOS 7.6 阿里云远程服务器 文章目录 前言️正文1、FILE 结构设计2、函数使用及分析3、文件打开 fopen4、文件关闭 fclose5、缓冲区刷新 fflush6、数据写入 fwrite7、数据读取 fread8、实际效果9、小结10、源码 总结 前言
在 C语言 的文件流中存在一个 FILE 结构体类型其中包含了文件的诸多读写信息以及重要的文件描述符 fd在此类型之上诞生了 C语言 文件相关操作如 fopen、fclose、fwrite 等这些函数本质上都是对系统调用的封装因此我们可以根据系统调用和缓冲区相关知识模拟实现出一个简单的 C语言 文件流 本文重点 模拟实现 FILE 及 C语言 文件操作相关函数
注意 本文实现的只是一个简单的 demo重点在于理解系统调用及缓冲区 ️正文
1、FILE 结构设计
在设计 FILE 结构体前首先要清楚 FILE 中有自己的缓冲区及冲刷方式 图片来源《Linux基础IO》 - 2021dragon
缓冲区的大小和刷新方式因平台而异这里我们将 大小设置为 1024 刷新方式选择 行缓冲为了方便对缓冲区进行控制还需要一个下标 _current当然还有 最重要的文件描述符 _fd
#define BUFFER_SIZE 1024 //缓冲区大小//通过位图的方式控制刷新方式
#define BUFFER_NONE 0x1 //无缓冲
#define BUFFER_LINE 0x2 //行缓冲
#define BUFFER_ALL 0x4 //全缓冲typedef struct MY_FILE
{char _buffer[BUFFER_SIZE]; //缓冲区size_t _current; //缓冲区下标int _flush; //刷新方式位图结构int _fd; //文件描述符
}MY_FILE;当前模拟实现的 FILE 只具备最基本的功能重点在于呈现原理
在模拟实现 C语言 文件操作相关函数前需要先来简单回顾下 2、函数使用及分析
主要实现的函数有以下几个
fopen 打开文件fclose 关闭文件fflush 进行缓冲区刷新fwrite 对文件中写入数据fread 读取文件数据
#include stdio.h
#include assert.h
#include string.hint main()
{//打开文件写入数据FILE* fp fopen(file.txt, w);assert(fp);const char* str 露易斯湖三面环山层峦叠嶂翠绿静谧的湖泊在宏伟山峰及壮观的维多利亚冰川的映照下更加秀丽迷人;char buff[1024] { 0 };snprintf(buff, sizeof(buff), str);fwrite(buff, 1, sizeof(buff), fp);fclose(fp);return 0;
}#include stdio.h
#include assert.h
#include string.hint main()
{//打开文件并从文件中读取信息FILE* fp fopen(file.txt, r);assert(fp);char buff[1024] { 0 };int n fread(buff, 1, sizeof(buff) - 1, fp);buff[n] \0;printf(%s, buff);fclose(fp);return 0;
}fopen
打开指定文件可以以多种方式打开若是以读方式打开时文件不存在会报错
fclose
根据 FILE* 关闭指定文件不能重复关闭
fwrite
对文件中写入指定数据一般是借助缓冲区进行写入
fread
读取文件数据同理一般是借助缓冲区先进行读取
不同的缓冲区有不同的刷新策略如果未触发相应的刷新策略会导致数据滞留在缓冲区中比如如果内存中的数据还没有刷新就断电的话会导致数据丢失除了通过特定方式进行缓冲区冲刷外还可以手动刷新缓冲区在 C语言 中手动刷新缓冲区的函数为 fflush
#include stdio.h
#include unistd.hint main()
{int cnt 20;while(cnt){printf(he); //故意不触发缓冲cnt--;if(cnt % 10 5) {fflush(stdout); //刷新缓冲区printf(\n当前已冲刷cnt: %d\n, cnt);}sleep(1);}return 0;
} 总的来说这些文件操作相关函数都是在对缓冲区进行写入及冲刷将数据拷贝给内核缓冲区再由内核缓冲区刷给文件 3、文件打开 fopen
MY_FILE *my_fopen(const char *path, const char *mode); //打开文件打开文件分为以下几步
根据传入的 mode 确认打开方式通过系统接口 open 打开文件创建 MY_FILE 结构体初始化内容返回创建好的 MY_FILE 类型
因为打开文件存在多种失败情况权限不对 / open 失败 / malloc 失败等所以当打开文件失败后需要返回 NULL
注意 假设是因 malloc 失败的那么在返回之前需要先关闭 fd否则会造成资源浪费
// 打开文件
MY_FILE *my_fopen(const char *path, const char *mode)
{assert(path mode);// 确定打开方式int flags 0; // 打开方式// 读O_RDONLY 读O_RDONLY | O_WRONLY// 写O_WRONLY | O_CREAT | O_TRUNC 写O_WRONLY | O_CREAT | O_TRUNC | O_RDONLY// 追加 O_WRONLY | O_CREAT | O_APPEND 追加O_WRONLY | O_CREAT | O_APPEND | O_RDONLY// 注意不考虑 b 二进制读写的情况if (*mode r){flags | O_RDONLY;if (strcmp(r, mode) 0)flags | O_WRONLY;}else if (*mode w || *mode a){flags | (O_WRONLY | O_CREAT);if (*mode w)flags | O_TRUNC;elseflags | O_APPEND;if (strcmp(w, mode) 0 || strcmp(a, mode) 0)flags | O_RDONLY;}else{// 无效打开方式assert(false);}// 根据打开方式打开文件// 注意新建文件需要设置权限int fd 0;if (flags O_CREAT)fd open(path, flags, 0666);elsefd open(path, flags);if (fd -1){// 打开失败的情况return NULL;}// 打开成功了创建 MY_FILE 结构体并返回MY_FILE *new_file (MY_FILE *)malloc(sizeof(MY_FILE));if (new_file NULL){// 此处不能断言需要返回空close(fd); // 需要先把 fd 关闭perror(malloc FILE fail!);return NULL;}// 初始化 MY_FILEmemset(new_file-_buffer, \0, BUFFER_SIZE); // 初始化缓冲区new_file-_current 0; // 下标置0new_file-_flush BUFFER_LINE; // 行刷新new_file-_fd fd; // 设置文件描述符return new_file;
}4、文件关闭 fclose
int my_fclose(MY_FILE *fp); //关闭文件文件在关闭前需要先将缓冲区中的内容进行冲刷否则会造成数据丢失
注意 my_fclose 返回值与 close 一致因此可以复用
// 关闭文件
int my_fclose(MY_FILE *fp)
{assert(fp);// 刷新残余数据if (fp-_current 0)my_fflush(fp);// 关闭 fdint ret close(fp-_fd);// 释放已开辟的空间free(fp);fp NULL;return ret;
}5、缓冲区刷新 fflush
int my_fflush(MY_FILE *stream); //缓冲区刷新缓冲区冲刷是一个十分重要的动作它决定着 IO 是否正确这里的 my_fflush 是将用户级缓冲区中的数据冲刷至内核级缓冲区
冲刷的本质拷贝用户先将数据拷贝给用户层面的缓冲区再系统调用将用户级缓冲区拷贝给内核级缓冲区最后才将数据由内核级缓冲区拷贝给文件
因此 IO 是非常影响效率的。数据传输过程必须遵循冯诺依曼体系结构
函数 fsync
将内核中的数据手动拷贝给目标文件内核级缓冲区的刷新策略极为复杂为了确保数据能正常传输可以选择手动刷新
注意 在冲刷完用户级缓冲区后write需要将缓冲区清空否则缓冲区就一直满载了
// 缓冲区刷新
int my_fflush(MY_FILE *stream)
{assert(stream);// 将数据写给文件int ret write(stream-_fd, stream-_buffer, stream-_current);stream-_current 0; // 每次刷新后都需要清空缓冲区fsync(stream-_fd); // 将内核中的数据强制刷给磁盘(文件)if (ret ! -1) return 0;else return -1;
}6、数据写入 fwrite
size_t my_fwrite(const void *ptr, size_t size, size_t nmemb, MY_FILE *stream); //数据写入数据写入用户级缓冲区的步骤
判断当前用户级缓冲区是否满载如果满了需要先刷新再进行后续操作获取当前待写入的数据大小 user_size 及用户级缓冲区剩余大小 my_size方便进行后续操作如果 my_size user_size说明缓冲区容量足够直接进行拷贝否则说明缓冲区容量不足需要重复冲刷-拷贝-再冲刷 的过程直到将数据全部拷贝拷贝完成后需要判断是否触发相应的刷新策略比如 行刷新-最后一个字符是否为 \n如果满足条件就刷新缓冲区数据写入完成返回实际写入的字节数简化版即 user_size
如果是一次写不完的情况需要通过循环写入数据并且在缓冲区满后进行刷新因为循环写入时目标数据的读取位置是在不断变化的一次读取一部分不断后移所以需要对读取位置和读取大小进行特殊处理
// 数据写入
size_t my_fwrite(const void *ptr, size_t size, size_t nmemb, MY_FILE *stream)
{// 写入先判断缓冲区是否已满if (stream-_current BUFFER_SIZE)my_fflush(stream);size_t user_size size * nmemb; // 用户想写入的字节数size_t my_size BUFFER_SIZE - stream-_current; // 缓冲区中剩余可用空间size_t writen 0; // 成功写入数据的大小if (my_size user_size){// 直接可用全部写入memcpy(stream-_buffer stream-_current, ptr, user_size);stream-_current user_size;writen user_size;}else{// 一次写不完需要分批写入size_t tmp user_size; // 用于定位 ptr 的读取位置while (user_size my_size){// 一次写入 my_size 个数据。user_size 会减小memcpy(stream-_buffer stream-_current, ptr (tmp - user_size), my_size);stream-_current my_size; // 切记实时更新下标my_fflush(stream); // 写入后刷新缓冲区user_size - my_size;my_size BUFFER_SIZE - stream-_current;}// 最后空间肯定足够再把数据写入缓冲区中memcpy(stream-_buffer stream-_current, ptr (tmp - user_size), user_size);stream-_current user_size;writen tmp;}// 通过刷新方式判断是否进行刷新if (stream-_flush BUFFER_NONE){// 无缓冲直接冲刷my_fflush(stream);}else if (stream-_flush BUFFER_LINE){// 行缓冲遇见 \n 才刷新if (stream-_buffer[stream-_current - 1] \n)my_fflush(stream);}else{// 全缓冲满了才刷新if (stream-_current BUFFER_SIZE)my_fflush(stream);}// 为了简化这里返回用户实际写入的字节数即 user_sizereturn writen;
}7、数据读取 fread
在进行数据读取时需要经历 文件-内核级缓冲区-用户级缓冲区-目标空间 的繁琐过程并且还要考虑 用户级缓冲区是否能够一次读取完所有数据若不能则需要多次读取
注意
读取前如果用户级缓冲区中有数据的话需要先将数据刷新给文件方便后续进行操作读取与写入不同读取结束后需要考虑 \0 的问题在最后一个位置加如果不加的话会导致识别错误系统(内核)不需要 \0但C语言中的字符串结尾必须加 \0现在是 系统-用户C语言
// 数据读取
size_t my_fread(void *ptr, size_t size, size_t nmemb, MY_FILE *stream)
{// 数据读取前需要先把缓冲区刷新if (stream-_current 0)my_fflush(stream);size_t user_size size * nmemb;size_t my_size BUFFER_SIZE;// 先将数据读取到FILE缓冲区中再赋给 ptrif (my_size user_size){// 此时缓冲区中足够存储用户需要的所有数据只需要读取一次read(stream-_fd, stream-_buffer, my_size);memcpy(ptr, stream-_buffer, my_size);*((char *)ptr my_size - 1) \0;}else{int ret 1;size_t tmp user_size;while (ret){// 一次读不完需要多读取几次ret read(stream-_fd, stream-_buffer, my_size);stream-_buffer[ret] \0;memcpy(ptr (tmp - user_size), stream-_buffer, my_size);stream-_current 0;user_size - my_size;}}size_t readn strlen(ptr);return readn;
}8、实际效果
现在通过自己写的 myStdio 测试C语言文件流操作
#include stdio.h
#include string.h
#include assert.h
#include unistd.hint main()
{//打开文件写入一段话FILE* fp fopen(log.txt, w);assert(fp);char inPutBuff[512] 2110年1月1日距离地球能源完全枯竭还有3650天。为了解决地球能源危机\n人类制造了赛尔机器人和宇宙能源探索飞船赛尔号去寻找神秘精灵看守的无尽能源。;int n fwrite(inPutBuff, 1, strlen(inPutBuff), fp);printf(本次成功写入 %d 字节的数据, n);fclose(fp);printf(\n\n);//重新打开文件fp fopen(log.txt, r);assert(fp);char outPutBuff[512] { \0 };n fread(outPutBuff, 1, sizeof(outPutBuff), fp);printf(本次成功读取 %d 字节的数据具体内容为: \n%s\n, n, outPutBuff);fclose(fp);fp NULL;return 0;
}结果 下面是库函数的结果 可以看出结果是一样的 9、小结
用户在进行文件流操作时实际要进行至少三次的拷贝用户-用户级缓冲区-内核级缓冲区-文件C语言 中众多文件流操作都是在完成 用户-用户级缓冲区 的这一次拷贝动作其他语言也是如此最终都是通过系统调用将数据冲刷到磁盘文件中 此时上一篇文章中的最后一个例子为什么会打印两次 hello fprintf 就很好理解了因为没有触发刷新条件文件一般为全缓冲所以数据滞留在用户层缓冲区中fork 创建子进程后子进程结束刷新用户层缓冲区[子进程]此时会触发写时拷贝机制父子进程的用户层缓冲区不再是同一个父进程结束后刷新用户层缓冲区[父进程]因此会看见打印两次的奇怪现象 最后再简单提一下 printf 和 scanf 的工作原理 无论是什么类型最终都要转为字符型进行存储程序中的各种类型只是为了更好的解决问题 printf 根据格式读取数据如整型、浮点型并将其转为字符串定义缓冲区然后将字符串写入缓冲区stdout最后结合一定的刷新策略将数据进行冲刷 scanf 读取数据至缓冲区stdin根据格式将字符串扫描分割存入字符指针数组最后将字符串转为对应的类型赋值给相应的变量 这也就解释了为什么要确保 输出/输入 格式与数据匹配如果不匹配的话会导致 读取/赋值 错误 10、源码
关于 myStdio 的源码可以点击下方链接进行获取
模拟实现C语言文件流 总结
以上就是本次关于 Linux【模拟实现C语言文件流】的全部内容了通过 系统调用缓冲区我们模拟实现了一个简单版的 myStdio 库在模拟实现过程中势必会遇到很多问题而这些问题都能帮助你更好的理解缓冲区的本质提高 IO 效率 相关文章推荐 Linux基础IO【重定向及缓冲区理解】 Linux基础IO【文件理解与操作】 Linux【模拟实现简易版bash】 Linux进程控制【进程程序替换】 Linux进程控制【创建、终止、等待】