大致自定义文件I/O库函数的实现详解(了解即可)
目录
一、mystdio.h
代码思路分析
二、mystdio.c
1. 辅助函数 BuyFile
2. 文件打开函数 MyFopen
3. 文件关闭函数 MyFclose
4. 数据写入函数 MyFwrite
1、memcpy(file->outbuffer + file->bufferlen, str, len);
2、按位与(&)运算的作用
运算规则
5. 缓冲区刷新函数 MyFFlush
1、缓冲区空检查
2、数据写入系统调用
3、数据同步到磁盘
4、重置缓冲区
三、Makefile
四、Makefile-st
一、mystdio.h
#pragma once
// 这是一个预处理指令,确保头文件在编译时只被包含一次,防止重复包含#include <stdio.h>
// 包含标准输入输出库,提供基本的文件操作函数原型#define MAX 1024
// 定义输出缓冲区的大小为1024字节// 定义刷新模式的标志位
#define NONE_FLUSH (1<<0) // 不自动刷新
#define LINE_FLUSH (1<<1) // 行缓冲模式(遇到换行符时刷新)
#define FULL_FLUSH (1<<2) // 全缓冲模式(缓冲区满时刷新)// 自定义文件结构体,模拟标准库的FILE结构
typedef struct IO_FILE
{int fileno; // 文件描述符(底层操作系统文件标识)int flag; // 文件打开模式标志char outbuffer[MAX]; // 输出缓冲区int bufferlen; // 当前缓冲区中的数据长度int flush_method; // 缓冲刷新策略(NONE/LINE/FULL)
}MyFile;// 函数声明/*** @brief 打开文件并初始化MyFile结构体* @param path 文件路径* @param mode 打开模式("r"/"w"/"a"等)* @return 成功返回MyFile指针,失败返回NULL*/
MyFile *MyFopen(const char *path, const char *mode);/*** @brief 关闭文件并释放资源* @param fp MyFile指针*/
void MyFclose(MyFile *fp);/*** @brief 向文件写入数据* @param fp MyFile指针* @param str 要写入的数据指针* @param len 要写入的数据长度* @return 成功写入的字节数*/
int MyFwrite(MyFile *fp, void *str, int len);/*** @brief 手动刷新缓冲区* @param fp MyFile指针*/
void MyFFlush(MyFile *fp);
代码思路分析
这段代码实现了一个简单的文件I/O封装库,模拟了标准C库中的文件操作函数。主要特点包括:
-
缓冲机制:
-
使用
outbuffer
作为输出缓冲区,减少直接系统调用的次数 -
支持三种缓冲策略:无缓冲、行缓冲和全缓冲
-
-
结构设计:
-
MyFile
结构体封装了文件描述符、缓冲区及状态信息 -
类似于标准库的
FILE
结构,但更简化
-
-
功能模拟:
-
MyFopen
:模拟fopen
,打开文件并初始化结构体 -
MyFwrite
:模拟fwrite
,写入数据到缓冲区 -
MyFFlush
:模拟fflush
,强制刷新缓冲区 -
MyFclose
:模拟fclose
,关闭文件并释放资源
-
-
缓冲策略:
-
NONE_FLUSH
:每次写入都立即刷新(无缓冲) -
LINE_FLUSH
:遇到换行符时刷新(行缓冲) -
FULL_FLUSH
:缓冲区满时刷新(全缓冲)
-
二、mystdio.c
#include "mystdio.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>static MyFile *BuyFile(int fd, int flag)
{MyFile *f = (MyFile*)malloc(sizeof(MyFile));if(f == NULL) return NULL;f->bufferlen = 0;f->fileno = fd;f->flag = flag;f->flush_method = LINE_FLUSH;memset(f->outbuffer, 0, sizeof(f->outbuffer));return f;
}MyFile *MyFopen(const char *path, const char *mode)
{int fd = -1;int flag = 0;if(strcmp(mode, "w") == 0){flag = O_CREAT | O_WRONLY | O_TRUNC;fd = open(path, flag, 0666);}else if(strcmp(mode, "a") == 0){flag = O_CREAT | O_WRONLY | O_APPEND;fd = open(path, flag, 0666);}else if(strcmp(mode, "r") == 0){flag = O_RDWR;fd = open(path, flag);}else{//TODO}if(fd < 0) return NULL;return BuyFile(fd, flag);
}
void MyFclose(MyFile *file)
{if(file->fileno < 0) return;MyFFlush(file);close(file->fileno);free(file);
}
int MyFwrite(MyFile *file, void *str, int len)
{// 1. 拷贝memcpy(file->outbuffer+file->bufferlen, str, len);file->bufferlen += len;// 2. 尝试判断是否满足刷新条件!if((file->flush_method & LINE_FLUSH) && file->outbuffer[file->bufferlen-1] == '\n'){MyFFlush(file);}return 0;
}
void MyFFlush(MyFile *file)
{if(file->bufferlen <= 0) return;// 把数据从用户拷贝到内核文件缓冲区中int n = write(file->fileno, file->outbuffer, file->bufferlen);(void)n;fsync(file->fileno);file->bufferlen = 0;}
1. 辅助函数 BuyFile
BuyFile
函数在这个自定义 I/O 库中模拟的是标准 C 库(如 glibc)中 FILE
结构体的初始化过程。
/*** @brief 分配并初始化MyFile结构体* @param fd 文件描述符* @param flag 文件打开标志* @return 成功返回MyFile指针,失败返回NULL*/
static MyFile *BuyFile(int fd, int flag)
{// 分配MyFile结构体内存MyFile *f = (MyFile*)malloc(sizeof(MyFile));if(f == NULL) return NULL;// 初始化结构体成员f->bufferlen = 0; // 缓冲区初始为空f->fileno = fd; // 设置文件描述符f->flag = flag; // 设置文件打开标志f->flush_method = LINE_FLUSH; // 默认使用行缓冲模式memset(f->outbuffer, 0, sizeof(f->outbuffer)); // 清空缓冲区return f;
}
功能分析:
-
这是一个静态辅助函数,用于创建和初始化
MyFile
结构体 -
设置默认缓冲策略为
LINE_FLUSH
(行缓冲) -
清空输出缓冲区,确保初始状态干净
2. 文件打开函数 MyFopen
/*** @brief 打开文件并初始化MyFile对象* @param path 文件路径* @param mode 打开模式("w"/"a"/"r")* @return 成功返回MyFile指针,失败返回NULL*/
MyFile *MyFopen(const char *path, const char *mode)
{int fd = -1;int flag = 0;// 根据模式设置打开标志if(strcmp(mode, "w") == 0) // 写模式{flag = O_CREAT | O_WRONLY | O_TRUNC; // 创建文件/只写/清空内容fd = open(path, flag, 0666); // 设置文件权限为rw-rw-rw-}else if(strcmp(mode, "a") == 0) // 追加模式{flag = O_CREAT | O_WRONLY | O_APPEND; // 创建文件/只写/追加fd = open(path, flag, 0666);}else if(strcmp(mode, "r") == 0) // 读模式{flag = O_RDWR; // 读写模式fd = open(path, flag);}else{//TODO: 可以添加其他模式支持}// 检查文件是否成功打开if(fd < 0) return NULL;// 创建并初始化MyFile对象return BuyFile(fd, flag);
}
功能分析:
-
支持三种基本文件模式:写("w")、追加("a")和读("r")
-
使用系统调用
open
打开文件,获取文件描述符 -
根据模式设置不同的打开标志:
-
"w"模式会清空文件内容
-
"a"模式会在文件末尾追加
-
"r"模式允许读写
-
-
最终调用
BuyFile
创建并初始化MyFile
对象
3. 文件关闭函数 MyFclose
/*** @brief 关闭文件并释放资源* @param file MyFile对象指针*/
void MyFclose(MyFile *file)
{if(file->fileno < 0) return; // 检查文件描述符是否有效MyFFlush(file); // 确保缓冲区数据写入文件close(file->fileno); // 关闭文件描述符free(file); // 释放MyFile结构体内存
}
功能分析:
-
首先刷新缓冲区,确保所有数据写入文件
-
关闭底层文件描述符
-
释放
MyFile
结构体内存 -
包含安全检查,防止无效文件描述符
4. 数据写入函数 MyFwrite
/*** @brief 向文件写入数据* @param file MyFile对象指针* @param str 要写入的数据指针* @param len 要写入的数据长度* @return 总是返回0(实际实现可能需要改进)*/
int MyFwrite(MyFile *file, void *str, int len)
{// 1. 将数据拷贝到缓冲区memcpy(file->outbuffer + file->bufferlen, str, len);file->bufferlen += len; // 更新缓冲区长度// 2. 检查是否满足刷新条件if((file->flush_method & LINE_FLUSH) && // 如果是行缓冲模式file->outbuffer[file->bufferlen-1] == '\n') // 且最后一个字符是换行符{MyFFlush(file); // 刷新缓冲区}return 0; // 注意:实际应该返回写入的字节数,这里需要改进
}
功能分析:
1、memcpy(file->outbuffer + file->bufferlen, str, len);
memcpy(file->outbuffer + file->bufferlen, str, len);
-
file->outbuffer
:指向文件输出缓冲区的起始地址。 -
file->bufferlen
:当前缓冲区中已存储的数据长度(单位:字节)。 -
file->outbuffer + file->bufferlen
:计算缓冲区中下一个空闲位置的地址(即已存数据的末尾)。 -
str
:要写入的数据源地址(用户传入的字符串或二进制数据)。 -
len
:要拷贝的数据长度(单位:字节)。 -
memcpy
:标准内存拷贝函数,将str
的len
字节数据复制到目标地址。
2、按位与(&
)运算的作用
-
LINE_FLUSH
的定义(在头文件中):#define LINE_FLUSH (1 << 1) // 即二进制 0b10(十进制 2)
-
file->flush_method
是一个整型变量,存储当前的缓冲模式(可能是NONE_FLUSH
、LINE_FLUSH
或FULL_FLUSH
的组合)。
运算规则
-
按位与
&
会对两个数的 二进制位 逐位比较:-
如果某一位在两个数中都是
1
,结果的该位才是1
,否则是0
。
-
-
判断逻辑:
-
如果
file->flush_method
包含LINE_FLUSH
,则file->flush_method & LINE_FLUSH
结果非零(true
)。 -
如果不包含,则结果为
0
(false
)。
-
-
将数据直接拷贝到输出缓冲区
-
实现了行缓冲逻辑:当检测到换行符时自动刷新
-
当前实现总是返回0,实际应该返回写入的字节数
-
缺少缓冲区满时的处理逻辑(全缓冲模式)
5. 缓冲区刷新函数 MyFFlush
/*** @brief 刷新缓冲区,将数据写入文件* @param file MyFile对象指针*/
void MyFFlush(MyFile *file)
{if(file->bufferlen <= 0) return; // 缓冲区为空则直接返回// 将数据从用户缓冲区写入内核缓冲区int n = write(file->fileno, file->outbuffer, file->bufferlen);(void)n; // 忽略返回值(实际实现应该检查)fsync(file->fileno); // 确保数据写入磁盘file->bufferlen = 0; // 重置缓冲区长度
}
功能分析:
-
将缓冲区内容写入底层文件描述符
-
使用
fsync
确保数据持久化到磁盘 -
重置缓冲区长度为0
-
当前实现忽略了
write
的返回值,实际应该处理写入错误情况
1、缓冲区空检查
if(file->bufferlen <= 0) return;
-
作用:如果缓冲区中没有数据,直接返回
-
优化意义:避免不必要的系统调用
-
潜在问题:没有检查
file
是否为 NULL(健壮性考虑)
2、数据写入系统调用
int n = write(file->fileno, file->outbuffer, file->bufferlen);
(void)n; // 忽略返回值
-
write
系统调用:-
参数1:文件描述符
-
参数2:要写入的数据缓冲区
-
参数3:要写入的字节数
-
返回值:实际写入的字节数(可能小于请求的字节数)
-
-
问题:
-
完全忽略了
write
的返回值,可能导致部分写入未被处理 -
没有错误处理(如
write
返回 -1 表示出错)
-
3、数据同步到磁盘
fsync(file->fileno);
-
fsync
系统调用:-
强制将内核缓冲区中的数据写入物理磁盘
-
确保数据持久化,不会因系统崩溃而丢失
-
-
性能影响:
-
这是一个昂贵的操作,会显著降低 I/O 性能
-
通常只在需要强一致性保证时使用
-
4、重置缓冲区
file->bufferlen = 0;
-
作用:将缓冲区长度重置为0,表示缓冲区已空
-
实现方式:
-
只是简单地重置长度计数器
-
没有清空缓冲区内容(可能的安全问题)
-
三、Makefile
# 生成动态链接库 libmyc.so,依赖 mystdio.o 和 mystring.o 两个目标文件
# $@ 表示目标文件(libmyc.so),$^ 表示所有依赖文件
libmyc.so: mystdio.o mystring.ogcc -shared -o $@ $^# 编译 mystdio.c 生成位置无关代码(PIC)的目标文件
# -fPIC 选项是生成动态库所必需的
# $< 表示第一个依赖文件(mystdio.c)
mystdio.o: mystdio.cgcc -fPIC -c $<# 编译 mystring.c 生成位置无关代码(PIC)的目标文件
mystring.o: mystring.cgcc -fPIC -c $<# 伪目标:打包输出库文件
.PHONY: output
output:# 创建发布目录结构mkdir -p lib/include # 存放头文件的目录mkdir -p lib/mylib # 存放动态库的目录# 拷贝所有头文件到include目录cp -f *.h lib/include# 拷贝动态库文件到mylib目录cp -f *.so lib/mylib# 将整个lib目录打包压缩tar czf lib.tgz lib# 伪目标:清理生成的文件
.PHONY: clean
clean:# 删除所有目标文件、动态库文件和生成的目录rm -rf *.o libmyc.so lib lib.tgz
四、Makefile-st
# 生成静态链接库 libmyc.a,依赖 mystdio.o 和 mystring.o 两个目标文件
# 注意:与动态库(.so)不同,静态库(.a)在编译时会被完整嵌入可执行文件
# $@ 表示目标文件(libmyc.a),$^ 表示所有依赖文件
libmyc.a: mystdio.o mystring.o# 使用 ar 命令打包目标文件生成静态库# -r 替换现有文件 -c 创建新库ar -rc $@ $^# 编译 mystdio.c 生成目标文件
# 注意:静态库不需要 -fPIC 位置无关代码选项(与动态库不同)
# $< 表示第一个依赖文件(mystdio.c)
mystdio.o: mystdio.cgcc -c $<# 编译 mystring.c 生成目标文件
mystring.o: mystring.cgcc -c $<# 伪目标:打包发布静态库文件
.PHONY: output
output:# 创建标准的库发布目录结构mkdir -p lib/include # 存放头文件(.h)的目录mkdir -p lib/mylib # 存放静态库文件(.a)的目录# 拷贝所有头文件到include目录cp -f *.h lib/include# 拷贝静态库文件到mylib目录(注意这里是.a文件而非.so)cp -f *.a lib/mylib# 将整个lib目录打包压缩,便于分发tar czf lib.tgz lib# 伪目标:清理生成的文件
.PHONY: clean
clean:# 删除编译过程中生成的所有中间文件# 包括:目标文件(.o)、静态库文件(.a)、打包目录和压缩包rm -rf *.o libmyc.a lib lib.tgz