探寻Linux下C语言文件流的诗意实现

发布时间:2026/5/22 7:04:43

探寻Linux下C语言文件流的诗意实现 一、前言文件流的哲学在C语言中文件流是一种抽象的概念它将复杂的文件操作简化为一系列流畅的读写动作。标准库中的FILE结构体及其相关函数如fopen()、fread()、fwrite()等为程序员提供了一个优雅的接口使我们能够专注于数据的处理而非底层的细节。然而这些优雅的接口背后隐藏着怎样的奥秘今天我们将揭开这层面纱亲手构建一个简化版的文件流系统。在这里插入图片描述本文重点 模拟实现FILE及C语言文件操作相关函数注意 本文实现的只是一个简单的 demo重点在于理解系统调用及缓冲区️正文一、FILE 结构设计在设计 FILE 结构体前首先要清楚 FILE 中有自己的缓冲区及冲刷方式在这里插入图片描述缓冲区的大小和刷新方式因平台而异这里我们将 大小设置为1024刷新方式选择行缓冲为了方便对缓冲区进行控制还需要一个下标_current当然还有 最重要的文件描述符_fd代码语言javascriptAI代码解释#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语言文件操作相关函数前需要先来简单回顾下二、函数使用及分析主要实现的函数有以下几个fopen 打开文件fclose 关闭文件fflush 进行缓冲区刷新fwrite 对文件中写入数据fread 读取文件数据代码语言javascriptAI代码解释#include stdio.h #include assert.h #include string.h int 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; }在这里插入图片描述代码语言javascriptAI代码解释#include stdio.h #include assert.h #include string.h int 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代码语言javascriptAI代码解释#include stdio.h #include unistd.h int 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; }在这里插入图片描述在cnt15和5时先手动冲刷两次之后程序结束自动全部冲刷总的来说这些文件操作相关函数都是在对缓冲区进行写入及冲刷将数据拷贝给内核缓冲区再由内核缓冲区刷给文件2.1、文件打开 fopen代码语言javascriptAI代码解释MY_FILE *my_fopen(const char *path, const char *mode); //打开文件打开文件分为以下几步根据传入的mode确认打开方式通过系统接口open打开文件创建 MY_FILE 结构体初始化内容返回创建好的 MY_FILE 类型因为打开文件存在多种失败情况权限不对/open 失败/malloc 失败等所以当打开文件失败后需要返回NULL注意 假设是因 malloc 失败的那么在返回之前需要先关闭 fd否则会造成资源浪费// 打开文件代码语言javascriptAI代码解释// 打开文件 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; else flags | 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); else fd 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_FILE memset(new_file-_buffer, \0, BUFFER_SIZE); // 初始化缓冲区 new_file-_current 0; // 下标置0 new_file-_flush BUFFER_LINE; // 行刷新 new_file-_fd fd; // 设置文件描述符 return new_file; }2.2、文件关闭 fclose代码语言javascriptAI代码解释int my_fclose(MY_FILE *fp); //关闭文件文件在关闭前需要先将缓冲区中的内容进行冲刷否则会造成数据丢失注意my_fclose返回值与close一致因此可以复用代码语言javascriptAI代码解释// 关闭文件 int my_fclose(MY_FILE *fp) { assert(fp); // 刷新残余数据 if (fp-_current 0) my_fflush(fp); // 关闭 fd int ret close(fp-_fd); // 释放已开辟的空间 free(fp); fp NULL; return ret; }2.3、缓冲区刷新 fflush代码语言javascriptAI代码解释int my_fflush(MY_FILE *stream); //缓冲区刷新缓冲区冲刷是一个十分重要的动作它决定着 IO 是否正确这里的my_fflush是将用户级缓冲区中的数据冲刷至内核级缓冲区冲刷的本质拷贝用户先将数据拷贝给用户层面的缓冲区再系统调用将用户级缓冲区拷贝给内核级缓冲区最后才将数据由内核级缓冲区拷贝给文件因此IO是非常影响效率的。数据传输过程必须遵循冯诺依曼体系结构函数fsync将内核中的数据手动拷贝给目标文件内核级缓冲区的刷新策略极为复杂为了确保数据能正常传输可以选择手动刷新注意 在冲刷完用户级缓冲区后write需要将缓冲区清空否则缓冲区就一直满载了代码语言javascriptAI代码解释// 缓冲区刷新 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; }2.3、数据写入 fwrite代码语言javascriptAI代码解释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如果是一次写不完的情况需要通过循环写入数据并且在缓冲区满后进行刷新因为循环写入时目标数据的读取位置是在不断变化的一次读取一部分不断后移所以需要对读取位置和读取大小进行特殊处理2.4、数据读取 fread在进行数据读取时需要经历文件-内核级缓冲区-用户级缓冲区-目标空间的繁琐过程并且还要考虑用户级缓冲区是否能够一次读取完所有数据若不能则需要多次读取注意读取前如果用户级缓冲区中有数据的话需要先将数据刷新给文件方便后续进行操作读取与写入不同读取结束后需要考虑 \0 的问题在最后一个位置加如果不加的话会导致识别错误系统(内核)不需要 \0但C语言中的字符串结尾必须加 \0现在是 系统-用户C语言代码语言javascriptAI代码解释// 数据读取 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缓冲区中再赋给 ptr if (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; }2.6 小结用户在进行文件流操作时实际要进行至少三次的拷贝用户-用户级缓冲区-内核级缓冲区-文件C语言 中众多文件流操作都是在完成用户-用户级缓冲区的这一次拷贝动作其他语言也是如此最终都是通过系统调用将数据冲刷到磁盘文件中在这里插入图片描述最后再简单提一下printf和scanf的工作原理无论是什么类型最终都要转为字符型进行存储程序中的各种类型只是为了更好的解决问题printf根据格式读取数据如整型、浮点型并将其转为字符串定义缓冲区然后将字符串写入缓冲区stdout最后结合一定的刷新策略将数据进行冲刷scanf读取数据至缓冲区stdin根据格式将字符串扫描分割存入字符指针数组最后将字符串转为对应的类型赋值给相应的变量这也就解释了为什么要确保输出/输入格式与数据匹配如果不匹配的话会导致读取/赋值错误本篇关于文件操作模拟实现的介绍就暂告段落啦希望能对大家的学习产生帮助欢迎各位佬前来支持斧正

相关新闻