引言
- 前面铺垫了那么多,包括扇区管理、创建文件、删除文件等等,感觉都快忘记主线任务了,这些都不是我们的目的,我们的目的是有效的存取数据,而前面做的工作都只是准备工作,本章节我们就来实现文件读写这一核心功能
文件描述符
- 我们在应用编程中,操作文件通常分为三步:1.打开(open) 2.读/写(read/write) 3.关闭(close)
- 在实现文件读写之前,我们还有一个工作没有做,那就是实现文件的打开/关闭功能
- 在实现代码之前,我们必须提前思考下面的问题
- 打开一个文件意味着什么?为什么不是直接读写文件呢?
- 读写文件数据时必须要实时操作硬盘吗?
- 如何指定文件的读写位置
- 开始写代码吧,首先我们肯定需要设计一种数据结构,用来表示文件的状态,如:文件名、文件长度、读写位置等,可以取个名字,就叫文件描描述符吧
typedef struct FILE_DESC { LIST_NODE node; FILE_ENTRY fe; U32 secIdx; // 文件数据链表节点偏移 U32 offset; // 用于记录文件读写位置 U08 changed; // 数据是否被修改 U08 catch[SEC_SIZE]; // 数据缓冲区 } FILE_DESC;
- 从上面的数据结构中,我们看到第一个元素:LIST_NODE node,这说明我们想将打开的文件串联成链表,为啥要串联成链表,当然是为了更好的管理了
- 第二个元素为:FILE_ENTRY fe,因为 FILE_ENTRY 数据结构中已经包含了一些文件的基本信息,而这些信息也是我们所需要的
- offset 元素是用于记录文件的读写位置的,其与 catch[SEC_SIZE] 是数据缓冲区搭配使用,我们对文件进行读写操作时都可以借助这个缓冲区来临时存放数据,我们在对文件进行读写时,不可能一下子把整个文件都读出来放到缓冲区中,而是一个扇区一个扇区的操作,catch[SEC_SIZE] 就是用来临时保存一个扇区的文件数据的。
- changed 元素很好理解,为 1 时表示文件有改动,为 0 表示无改动
- secIdx 元素的作用我们需要单独说明一下,文件的大小有可能超过 512 字节,于是就需要分配多个扇区以供使用,在前面章节中我们就已经知道,这些扇区号被串联成一个链表,secIdx 就表示当前操作的是第几个链表节点
- 我们知道,想要读写扇区,就必须知道其物理扇区号,所以我们得想办法由偏移量 secIdx 能得到其对应的物理扇区号,利用 NextSector() 函数就可以实现,具体如下:
static U32 FindIndex(U32 secBegin, U32 idx) { U32 ret = secBegin; U32 i = 0; while(i < idx && ret != SEC_END_FLAG) { ret = NextSector(ret); i++; } return ret; }
打开文件
- 为了更好的管理已经打开的文件,我们可以使用链表来管理这些文件,于是定义一个文件描述符链表头 static LIST fdList; 每打开一个文件,则创建一个描述符数据结构并将这个数据结构添加到 fdList 链表中
- 更进一步:文件的打开本质上就是创建一个文件描述符并添加到 fdList 链表;文件的关闭就相当于从 fdList 链表中删除对应的文件描述符节点
- 再实现打开文件之前,我们解决一下前面章节中遗留的问题:判断文件是否打开,原理很简单,遍历 fdList 链表,根据文件名匹配,如果匹配成功,则说明该文件已经打开
static BOOL IsOpened(const U08* name) { BOOL ret = FALSE; LIST_NODE* pListNode = NULL; FILE_DESC* fd = NULL; LIST_FOR_EACH(&fdList, pListNode) { fd = (FILE_DESC *)LIST_NODE(pListNode, FILE_DESC, node); if(TRUE == StrCmp(fd->fe.name, name, -1)) { ret = TRUE; break; } } return ret; }
- 实现打开文件功能,核心步骤为:创建 fd,填充 fd,插入链表 fdList
FILE_DESC* FileOpen(const U08* name) { FILE_DESC* fd = NULL; FILE_ENTRY* fe = NULL; // 检查参数合法性 if(NULL == name) goto error; // 文件不可以重复打开 if(TRUE == IsOpened(name)) goto error; // 找到 fe fe = FindInRoot(name); if(NULL == fe) goto error; // 申请一块内存用于存放文件描述符数据结构 fd = (FILE_DESC *)FS_MALLOC(sizeof(FILE_DESC)); if(NULL == fd) goto error; // 初始化文件描述符 fd,然后将其添加到链表 fdList 中 fd->fe = *fe; fd->secIdx = 0; fd->offset = 0; fd->changed =0; ListAddHead(&fdList, &(fd->node)); error: FS_FREE(fe); return fd; }
关闭文件
- 文件的关闭与打开正好是反过来的,不过关闭文件要比打开文件多一步,就是关闭文件之前需要将缓冲区中的文件数据写回硬盘中
- 下面是关闭文件的代码实现:
E_RET FileClose(FILE_DESC* fd) { // 检查文件描述符 fd 是否有效 if(FALSE == IsFdValid(fd)) return E_ERR; Flush(fd); // 将缓冲区中文件数据写到硬盘中 ListDelNode(&(fd->node)); // 删除链表节点 FS_FREE(fd); return E_OK; }
- 以上代码中 Flush(fd) 的功能就是将数据写回硬盘,数据缓冲区是 fd->catch[SEC_SIZE],想要将其写入硬盘,就必须知道其对应的物理扇区号,而这个扇区号可以通过调用 FindIndex() 函数实现。需要注意的是,不光是 catch 缓冲区数据需要写回硬盘, 对应的 FILE_ENTRY 也需要写回硬盘,因为在写入数据后,该数据结构也是有可能更改的,比如文件追加写入数据后 lastBytes 会增加
static E_RET Flush(FILE_DESC* fd) { U32 secNum = SEC_END_FLAG; U08* buf = NULL; E_RET ret = E_ERR; // 当参数 fd 为空或者缓冲区没有变动时直接退出 if(NULL == fd || 0 == fd->changed) return E_ERR; // 得到当前变动缓冲区对应的物理扇区号,然后将改动后的 catch[SEC_SIZE] 数据该扇区中 secNum = FindIndex(fd->fe.fileBegin, fd->secIdx); if(SEC_END_FLAG == secNum) return E_ERR; if(E_ERR == FS_WRITE(secNum, fd->catch)) return E_ERR; fd->changed = 0; // 缓冲区数据已成功写入硬盘,清变动标志 // 当文件数据更新后,该文件对应的 FILE_ENTRY 也有可能变化 // 比如文件追加写入数据后 lastBytes 会增加,此时需要重新将 FILE_ENTRY 写回硬盘 // 这里为了简单就不判断 FILE_ENTRY 是否改动了,直接重新写回硬盘 buf = (U08 *)FS_MALLOC(SEC_SIZE); if(NULL == buf) return E_ERR; if(E_ERR == FS_READ(fd->fe.inSecIdx, buf)) goto error; *((FILE_ENTRY *)(buf + fd->fe.inSecOff)) = fd->fe; if(E_ERR == FS_WRITE(fd->fe.inSecIdx, buf)) goto error; ret = E_OK; error: FS_FREE(buf); return ret; }
测试文件打开与关闭
- 写个代码代码简单测试一下文件的打开和关闭功能
void FsTest(void) { printk("creat:%d\n", CreatFileInRoot("1.txt")); FILE_DESC* fd = FileOpen("1.txt"); printk("%d\n", IsOpened("1.txt")); FileClose(fd); printk("%d\n", IsOpened("1.txt")); }
- 测试结果:
- 完整代码见:fs.c
文件写操作
- 在解决了文件的打开关闭函数后,接下来我们来研究一个文件的写操作
- 自然的,我们需要思考下面的几个问题
- 需要写入的数据量与文件读写指针的关系,当 catch 缓冲区满了之后该如何处理?很简单,只需要将当前缓冲区数据写入硬盘,然后将下一个扇区中的数据读入 catch 缓冲区,从 catch 缓冲区头部继续写接下来的数据
- 写入数据时,文件数据链表是如何变化的?这个问题很简单,文件写入数据较大时,需要申请空闲扇区,数据链表会继续向后追加
- 开始写代码,首先遍历 fdList, 确保传入的参数 fd 是个已经打开的文件,然后将 buf 中数据分批次写入文件数据区,因为一次最多只能写 SEC_SIZE 个字节的数据
U32 FileWrite(FILE_DESC* fd, U08* buf, U32 len) { U32 ret = 0; U32 i = 0; U32 n = 0; U32 secNum = SEC_END_FLAG; U08* feBuf = NULL; FILE_ENTRY* fe = NULL; U32 remainLen = 0; // 检查参数合法性 if(NULL == fd || NULL == buf || 0 == len) goto error; // 检查文件描述符 fd 是否有效 if(FALSE == IsFdValid(fd)) goto error; if(0 == fd->fe.fileNum) // 如果还没有分配文件数据区,则分配一个扇区 { if(E_ERR == FileAddNewSector(&fd->fe)) goto error; // FileAddNewSector 函数有可能改变硬盘中 fe 数据,所以我们还得把硬盘中 fe 数据复制到 fd->fe 中 feBuf = (U08 *)FS_MALLOC(SEC_SIZE); if(NULL == feBuf) goto error; if(E_ERR == FS_READ(fd->fe.inSecIdx, (U08 *)feBuf)) // 读出 fe 所在的整个扇区数据 goto error; fe = (FILE_ENTRY *)(feBuf + fd->fe.inSecOff); // 找到 fe 起始位置 fd->fe = *fe; } // 将 buf 中数据分批次写入文件数据区,一次最多只能写 SEC_SIZE 个字节的数据 while(i < len) { // 检查一下文件数据链表的最后一个扇区是否已经满了 // 如果满了,则申请一个空闲扇区,添加到 fe 对应的文件数据区链表末尾 if(SEC_SIZE == fd->offset) { if(E_ERR == FileAddNewSector(&fd->fe)) goto error; // FileAddNewSector 函数有可能改变硬盘中 fe 数据,所以我们还得把硬盘中 fe 数据复制到 fd->fe 中 feBuf = (U08 *)FS_MALLOC(SEC_SIZE); if(NULL == feBuf) goto error; if(E_ERR == FS_READ(fd->fe.inSecIdx, (U08 *)feBuf)) // 读出 fe 所在的整个扇区数据 goto error; fe = (FILE_ENTRY *)(feBuf + fd->fe.inSecOff); // 找到 fe 起始位置 fd->fe = *fe; fd->offset = 0; fd->secIdx++; } secNum = FindIndex(fd->fe.fileBegin, fd->secIdx); // 获得当前 catch 对应的物理扇区号 if(E_ERR == FS_READ(secNum, fd->catch)) // 将扇区号 secNum 中的数据读到 catch 缓冲区中 goto error; n = SEC_SIZE - fd->offset; // catch 缓冲区中剩余大小 remainLen = len - i; // 剩余长度 n = (n < remainLen ) ? n : remainLen; // 取 n 与 remainLen 较小值作为内存拷贝长度 MemCpy((U08 *)(fd->catch + fd->offset), (U08 *)(buf + i), n); i += n; fd->changed = 1; fd->offset += n; // 如果是最后一包的话,则更新 lastBytes if((fd->secIdx == fd->fe.fileNum - 1) && (fd->fe.lastBytes < fd->offset)) fd->fe.lastBytes = fd->offset; Flush(fd); ret = i; } error: FS_FREE(feBuf); return ret; }
- 在写数据之前,要判断一下文件的数据区是否足够,不够的话当然是要继续申请空闲扇区然后添加到文件数据链表的末尾,函数 FileAddNewSector() 就是实现该功能的
static E_RET FileAddNewSector(FILE_ENTRY* fe) { E_RET ret = E_ERR; U32 sn = SEC_END_FLAG; U08* buf = NULL; if(0 == fe->fileNum) // 如果尚未分配扇区,则分配一个空闲扇区 { sn = AllocSector(); // 申请一个空闲扇区 if(SEC_END_FLAG == sn) goto error; buf = (U08 *)FS_MALLOC(SEC_SIZE); if(NULL == buf) goto error; if(E_ERR == FS_READ(fe->inSecIdx, (U08 *)buf)) // 读出 fe 所在的整个扇区数据 goto error; fe = (FILE_ENTRY *)(buf + fe->inSecOff); // 找到 fe 起始位置 // 更新 fe fe->fileBegin = sn; fe->fileNum++; if(E_ERR == FS_WRITE(fe->inSecIdx, (U08 *)buf)) goto error; } else { sn = AllocSector(); if(SEC_END_FLAG == sn) goto error; buf = (U08 *)FS_MALLOC(SEC_SIZE); if(NULL == buf) goto error; if(E_ERR == FS_READ(fe->inSecIdx, (U08 *)buf)) // 读出 fe 所在的整个扇区数据 goto error; fe = (FILE_ENTRY *)(buf + fe->inSecOff); // 找到 fe 起始位置 if(E_ERR == AddToLast(fe->fileBegin, sn)) // 将新申请的扇区号 sn 添加到根目录扇区链表末尾 goto error; // 更新 fe fe->fileNum++; fe->lastBytes = 0; if(E_ERR == FS_WRITE(fe->inSecIdx, (U08 *)buf)) // 将更新后的数据写回硬盘 goto error; } ret = E_OK; error: FS_FREE(buf); return ret; }
- 写个代码测试一下文件写入函数是否正确。因为还没有实现文件读操作函数,所以测试时候直接根据扇区号从硬盘中读取数据
U32 testBuf[400]; void FsTest(void) { U32 sn = SEC_END_FLAG; U32 next = SEC_END_FLAG; // 当做文件数据 for(U32 i = 0; i < sizeof(testBuf) / sizeof(testBuf[0]); i++) testBuf[i] = i; printk("creat:%d\n", CreatFileInRoot("1.txt")); FILE_DESC* fd = FileOpen("1.txt"); printk("-------------- Write -------------\n"); // 将 testBuf 缓冲区中数据写入文件 printk("write:%d\n", FileWrite(fd, (U08 *)testBuf, sizeof(testBuf))); FileClose(fd); printk("-------------- Read -------------\n"); // 清 testBuf 缓冲区 for(U32 i = 0; i < sizeof(testBuf) / sizeof(testBuf[0]); i++) testBuf[i] = 0; // 重新打开文件 fd = FileOpen("1.txt"); printk("fileNum = %d\n", fd->fe.fileNum); printk("lastBytes = %d\n", fd->fe.lastBytes); printk("fileBegin = %d\n", fd->fe.fileBegin); next = fd->fe.fileBegin; for(U32 i = 0; i < fd->fe.fileNum; i++) { FS_READ(next, (U08 *)testBuf); printk("testBuf[0] = %d testBuf[1] = %d\n", testBuf[0], testBuf[1]); next = NextSector(next); printk("next = %d\n", next); } FileClose(fd); }
- 完整代码见:fd.c
- 从下图运行结果可以看出文件成功写入 1600 字节,文件占 4 个扇区,最后一个扇区 64 字节,3*512+64 正好等于 1600,并且打印出的每个扇区前两个字节数据也是正确的
文件读操作
- 实现了文件写入后,文件读取也就比较容易实现了
U32 FileRead(FILE_DESC* fd, U08* buf, U32 len) { U32 ret = 0; U32 i = 0; U32 n = 0; U32 readLen = 0; U32 secNum = SEC_END_FLAG; U32 remainLen = 0; // 检查参数合法性 if(NULL == fd || NULL == buf || 0 == len) goto error; // 检查文件描述符 fd 是否有效 if(FALSE == IsFdValid(fd)) goto error; n = GetFileLen(fd) - FileTell(fd); // 计算文件可读大小 readLen = (n < len) ? n : len; // 取文件大小与要读取长度 len 两者较小值 // 分批次读取,因为每次最多读取 SEC_SIZE 个字节的数据 while (i < readLen) { if(SEC_SIZE == fd->offset) { fd->offset = 0; fd->secIdx++; } secNum = FindIndex(fd->fe.fileBegin, fd->secIdx); // 获得当前 catch 对应的物理扇区号 if(E_ERR == FS_READ(secNum, fd->catch)) // 将扇区号 secNum 中的数据读到 catch 缓冲区中 goto error; n = SEC_SIZE - fd->offset; // catch 缓冲区中剩余大小 if(remainLen < SEC_SIZE) // 当剩余字节数小于 SEC_SIZE 时,n = remainLen n = remainLen; MemCpy((U08 *)(buf + i), (U08 *)(fd->catch + fd->offset), n); i += n; fd->offset += n; remainLen = readLen -i; ret = i; } error: return ret; }
- 需要注意的是,在文件读操作函数中,我们需要计算出文件可读大小,对应的语句为:n = GetFileLen(fd) - FileTell(fd);,可读大小等于文件总大小减去文件当前读写指针的位置
// 获取文件数据大小 U32 GetFileLen(FILE_DESC* fd) { U32 ret = 0; if(SEC_END_FLAG != fd->fe.fileBegin) ret = (fd->fe.fileNum - 1) * SEC_SIZE + fd->fe.lastBytes; return ret; } // 得到文件当前读写位置指针相对于文件首的偏移字节数 U32 FileTell(FILE_DESC* fd) { U32 ret = 0; if(SEC_END_FLAG != fd->fe.fileBegin) ret = fd->secIdx * SEC_SIZE + fd->offset; return ret; }
- 文件读写都已经实现好了,那么我们就可以来进行一个完整的读写测试了
U32 testBuf[300]; void FsTest(void) { U32 sn = SEC_END_FLAG; U32 next = SEC_END_FLAG; for(U32 i = 0; i < sizeof(testBuf) / sizeof(testBuf[0]); i++) testBuf[i] = i; printk("creat:%d\n", CreatFileInRoot("1.txt")); FILE_DESC* fd = FileOpen("1.txt"); // 将 testBuf 缓冲区中数据写入文件 printk("write:%d\n", FileWrite(fd, (U08 *)testBuf, sizeof(testBuf))); FileClose(fd); // 清 testBuf 缓冲区 for(U32 i = 0; i < sizeof(testBuf) / sizeof(testBuf[0]); i++) testBuf[i] = 0; // 重新打开文件 fd = FileOpen("1.txt"); // printk("Read:%d\n", FileRead(fd, (U08 *)testBuf, 2000)); // printk("Read:%d\n", FileRead(fd, (U08 *)testBuf, 800)); printk("Read:%d\n", FileRead(fd, (U08 *)testBuf, sizeof(testBuf))); for(U32 i = 0; i < sizeof(testBuf) / sizeof(testBuf[0]); i++) printk("%d ", testBuf[i]); FileClose(fd); }
- 测试结果如下,文件写入的数据与读取的数据相同,功能实现 OK
- 完整代码见:fs.c
设置文件读写指针位置
- 文件读写我们已经实现完成了,基本上文件系统的基本功能也差不多完成了,不过还有一个功能还需要实现,那就是设置文件读写指针位置,我们总不能每次读写文件都是从头开始吧
- 设置文件读写指针位置的代码其实非常简单,代码如下:
E_RET FileSeek(FILE_DESC* fd, U32 pos) { // 检查文件描述符 fd 是否有效 if(FALSE == IsFdValid(fd)) return E_ERR; pos = (pos < GetFileLen(fd)) ? pos : GetFileLen(fd); fd->secIdx = pos / SEC_SIZE; fd->offset = pos % SEC_SIZE; return E_OK; }
- 测试一下吧,测试代码跟文件读写的测试代码几乎一样,仅在读数据前重新设置一下读写指针位置
U32 testBuf[300]; void FsTest(void) { U32 sn = SEC_END_FLAG; U32 next = SEC_END_FLAG; for(U32 i = 0; i < sizeof(testBuf) / sizeof(testBuf[0]); i++) testBuf[i] = i; printk("creat:%d\n", CreatFileInRoot("1.txt")); FILE_DESC* fd = FileOpen("1.txt"); // 将 testBuf 缓冲区中数据写入文件 printk("write:%d\n", FileWrite(fd, (U08 *)testBuf, sizeof(testBuf))); FileClose(fd); // 清 testBuf 缓冲区 for(U32 i = 0; i < sizeof(testBuf) / sizeof(testBuf[0]); i++) testBuf[i] = 0; // 重新打开文件 fd = FileOpen("1.txt"); // 设置文件读写指针位置 FileSeek(fd, 100); // 打印当前文件读写指针位置 printk("FileTell:%d\n", FileTell(fd)); // printk("Read:%d\n", FileRead(fd, (U08 *)testBuf, 2000)); // printk("Read:%d\n", FileRead(fd, (U08 *)testBuf, 800)); printk("Read:%d\n", FileRead(fd, (U08 *)testBuf, sizeof(testBuf))); for(U32 i = 0; i < sizeof(testBuf) / sizeof(testBuf[0]); i++) printk("%d ", testBuf[i]); FileClose(fd); }
- 从结果上看,文件读写指针设置成功:
- 代码见:fs.c
系统调用
- 我们实现文件系统的目的不光是内核需要,应用层也需要,于是我们需要把相关函数设计成系统调用
- 关于系统调用如何实现这里就不再重复细说了,目前文件系统需要实现的系统调用接口函数有:
E_RET SYS_CreatFileInRoot(const U08* name); // 在根目录 root 下创建一个文件 E_RET SYS_DeleteFileInRoot(const U08* name); // 在根目录 root 下删除一个文件 E_RET SYS_RenameFileInRoot(const U08* old, const U08* new); // 文件重命名 FILE_DESC* SYS_FileOpen(const U08* name); // 打开文件 E_RET SYS_FileClose(FILE_DESC* fd); // 关闭文件 U32 SYS_FileWrite(FILE_DESC* fd, U08* buf, U32 len); // 向文件中写入数据 U32 SYS_FileRead(FILE_DESC* fd, U08* buf, U32 len); // 从文件中读取数据 U32 SYS_GetFileLen(FILE_DESC* fd); // 获取文件数据大小 U32 SYS_FileTell(FILE_DESC* fd); // 得到文件当前读写指针位置 E_RET SYS_FileSeek(FILE_DESC* fd, U32 pos); // 设置文件当前读写指针位置
- 系统调用实现涉及到的文件有:fs.c、fs.h、u_syscall.c、u_syscall.h
- 注意在实现系统调用时有个细节,那就是 user 层的文件描述符 fd 是 U32 类型的,而内核中是 FILE_DESC* 类型,为什么 user 层不使用 FILE_DESC* 类型定义呢?
- 因为 FILE_DESC 这个数据结构对于上层来说是不可见的,fd 值具体是多少对应用层来说并没有多大意义,而且应用层如果知道 FILE_DESC 具体定义,那么它就有可能做一些非法操作,user 层描述符 fd 定义为 U32 类型也是一种安全机制
- 在 “app.c” 中创建一个任务测试一下系统调用实现的是否有问题,FsTest() 函数其实就是我们上面做实验用的代码,原样复制过来而已,当然了打印 printk 替换成 print,代码见:app.c
static void TaskA(void) { SetCursorPos(0, 9); FsTest(); }
- 测试结果如下(由于打印超限,所以把之前 testBuf[300] 改为 testBuf[200])
- 文件系统暂时就写到这里吧