2 Star 6 Fork 3

稀风/KOS

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
文件系统设计与实现上.md 28.44 KB
一键复制 编辑 原始数据 按行查看 历史
稀风 提交于 2023-05-11 13:57 . 优化

引言

  • 能够读写硬盘扇区了是不是就可以创建文件了呢
  • 对于硬盘而言只有扇区的概念,没有文件的概念,文件是操作系统中的概念
  • 文件是有逻辑关联的数据集合,并且数据之间有存储上的先后关系
  • 硬盘和文件是如何联系在一起的呢

文件系统设计概要

  • 以前我们都是直接使用别人实现好的文件系统,而现在需要我们自己设计文件系统了,最先想到的文件系统设计如下

    • 每个扇区里存放一个文件
    • 每个扇区的第 0 个字节用于存储文件的大小,文件大小最大 511 字节
    • 通过扇区号查找文件,或者说文件名就是扇区号
  • 这种设计方式肯定是每个人都能想到的,然而这种设计并不具备可行性,缺陷太严重,文件名不友好,谁能通过一个数字(扇区号)来找到一个文件;文件的容量太小,一个文件只能放在一个扇区里,要是文件大小超过 512 字节了怎么办

  • 改进思路

    • 支持自定义文件名,通过字符串查找文件
    • 将文件名和扇区号做映射
    • 将多个扇区组织在一起,共同用于存储同一个文件
    • 例如:一个名为 “a.txt” 的文件,存于 13 号扇区中,但是一个扇区并不能存下该文件,还需要 35 号和 22 号两个扇区才能存下该文件(13 号扇区含有文件信息和部分文件内容)。如果文件继续增大,那么在后面继续分配扇区就可以了

    最最最简单的文件系统

  • 大致的方向有了,那么下面就要思考更多的细节问题了

    • 如何区分硬盘上的哪些扇区是空闲的,哪些已经被使用了?
    • 文件名和扇区号直接的映射本身又被存放在硬盘上的哪个位置?
    • 当文件增大时,如何分配扇区并维护扇区之间的关系?
  • 带着问题继续向下

  • 我们先来做一个文件系统的概要性设计

    文件系统概要设计

    • 第一块扇区中不光存放着文件系统基本信息,而且也存放着引导区程序,那么怎么将这个扇区既当引导区又作为数据区呢?扇区末尾存放 0x55AA(引导区标志),扇区开头预留 4 个字节的空间作为 jmp 指令,jmp 指令之后就可以存放文件系统基本信息相关数据。程序从 0 开始执行,通过 jmp 指令就可以跳转到指定的位置执行从而跳过中间的文件系统基本信息数据区
    • 1 号扇区用来存储根目录相关信息,文件系统最初至少需要一个目录作为入口,而这个入口就是根目录,试想一下如果没有根目录,比如前面的举例,a.txt 文件存储在 35 号扇区中,在没有根目录的情况下,我们如何得知 35 号扇区中存放着 a.txt 这个文件呢,总不能把所有的扇区都遍历一遍吧。于是我们规定一个固定的扇区存放根目录信息,通过这个根目录就可以一层一层的向下查找文件,直到找到我们想要的文件
    • 再往下就是扇区分配表了,用于记录扇区之间的逻辑关系,哪些扇区可用,哪些扇区不可用,隶属于同一文件的不同扇区之间的关系也存放在这个扇区分配表中
    • 最后就是文件数据区了,这里面存放的才是真正的文件数据

扇区分配表

  • 前面虽然对文件系统有了一个大概的认识,但是依旧迷迷糊糊的,为啥迷糊呢?因为扇区分配表与文件数据区之间的映射关系在我们脑子中还是一片空白

  • 那么接下来的重点就是搞清楚这个映射关系

  • 首先,我们能想到的逻辑关系是这样子的,当一个文件需要多个扇区共同存储时,只需要找到第一个扇区,之后就可以根据 next 指针依次向下寻找,直到找全所有扇区,是不是很像链表呢

    硬盘数据逻辑组织示意图

  • 逻辑上我们用链表的思维来设计映射关系很合适,但是这些映射关系是需要存储到硬盘中的,所以我们还得从物理上设计一下映射关系

    文件数据在物理上的映射关系

    • 从逻辑上看 next 指针和扇区数据是挨在一起的,然而在物理上,我们把 next 指针和扇区数据分开,我们把 next 指针统一放到一起,这片区域我们就称之为扇区分区表
    • 扇区分区表中的元素与文件数据区中的扇区是一一对应的,扇区分配表中每个元素我们分配 4 个字节大小,而文件数据区则以扇区为单位,既 512 字节
    • 还是以 a.txt 文件为示例,该文件的起始扇区号是 13,那么我们从扇区分配表中的找到第 13 号元素位置,该位置内存储一个数值 35,我们再找到第 35 号元素位置,从第 35 号元素位置中得到一个数值 22,然后我们再找到第 22 号元素位置,从该位置里我们得到了一个结束标志,表明查找结束了,于是我们依次获得了 13 25 22 这三个数值,那么 a.txt 这个文件的实际存储位置就是在 13 25 22 这三个扇区中

如何高效的管理扇区

  • 掌握了硬盘数据的逻辑组织方式和物理组织方式之后,我们也就清楚了文件的组织方式,对于大于一个扇区大小的文件,我们只需要申请多个空闲扇区分配给目标文件即可
  • 问题接踵而来,我们如何高效的查找以及获取空闲扇区呢?并且我们如何管理那些已经分配了的扇区呢?
  • 思路:
    • 扇区管理并不是对扇区本身进行管理,而是对扇区分配表中的各个元素进行管理
    • 将扇区分配表中的各个元素组织成不同类型的链表
    • 扇区分配表中的每一个链表代表一个文件
    • 未使用的扇区也组织成一个链表(空闲链表),当申请一个空闲扇区时,只需要从这个链表头取一个节点即可
    • 文件管理的过程就可以看做扇区在不同链表中的迁移过程

硬盘格式化准备

  • 使用硬盘的第一步就是格式化硬盘,格式化是指对硬盘分区进行的一种初始化操作,设置扇区分配表的初始状态。格式化会导致硬盘分区中的所有文件被清除
  • 换成代码其实就是实现一个初始化函数,具体要实现的功能有:建立引导区、根目录区和扇区分配表
  • 先来思考如何创建引导区,本质就是怎样去填充扇区 0 的 512 字节数据,总不能一个字节一个字节的往里填数据吧,于是我们设计下面的数据结构,将该数据结构统一存储到扇区 0 中,这样子对扇区 0 中的某个位置的读写就变成了对 FS_HEADER 结构体中元素的读写,更加方便直观
    // 存储与 0 号扇区,记录文件系统概要信息
    typedef struct FS_HEADER
    {
        U08     forJmp[4];          // 预留 4 字节给跳转指令使用
        U08     magic[32];          // 文件系统名字
        U32     sctNum;             // 可以使用的扇总数
        U32     mapNum;             // 扇区分配表所占的扇区数
        U32     freeBegin;          // 我们把空闲扇区组织成一个链表,freeBegin 为第一个空闲的扇区号
        U32     freeNum;            // 空闲扇区数   
    } FS_HEADER;
  • 同样的思路,我们再设计一个数据结构用来管理扇区 1,即根目录区。我们把目录(文件夹)当做一个稍微特殊文件,目录(文件夹)的本质其实就是文件
    // 记录根目录相关信息,存储于 1 号扇区
    typedef struct FS_ROOT
    {
        U08     magic[32];          // 根目录名字
        U32     rootBegin;          // 将根目录所涉及的所有扇区组织成一个链表,rootBegin 为第一个扇区号
        U32     rootNum;            // 根目录所占的扇区数   
        U32     lastBytes;          // 最后一个扇区中的有效数据字节数
    } FS_ROOT;
  • 格式化的关键:计算扇区分配表的大小
    • 已知硬盘总扇区数 max,这 max 个扇区并不是都是用作存储文件数据数据,还有一部分被用于存储扇区分配表
    • 假设有 n 个扇区被用于存储扇区分配表
    • 一个大小为 SEC_SIZE 的扇区有 (SEC_SIZE / 4) 个元素,那么 n 个扇区就一共可以管理着 (SEC_SIZE / 4) * n 个扇区(文件数据区)
    • 文件数据区的扇区数又可以表示为 max - 2 - n(扇区 0 和扇区 1 已被使用,需要减去)
    • 于是得到等式:(SEC_SIZE / 4) * n = max - 2 - n,推到得到扇区分配表的大小(所占扇区数):n = (max - 2) / ( (SEC_SIZE / 4) + 1)

硬盘格式化实现

  • 准备的差不多了,接下来进入主题,实现硬盘格式化功能函数

  • 在实现功能代码之前我们先扩大一下虚拟硬盘的大小,修改后的 Build.py

    hard_size = int(os.stat(kernel_bin).st_size/1024/1024) + 80
    cmd_str = "bximage -func=create " + IMG + ' -hd=' + str(hard_size) + ' -imgmode="flat" sectsize=512 -q'
    os.system(cmd_str)
  • 硬盘格式化本质上就是做了三件事:将文件系统概要信息填充到扇区 0,将根目录相关信息填充到扇区 1,以及初始化扇区分配表

  • 将文件系统概要信息填充到扇区 0 具体要做的工作是创建一个 FS_HEADER 数据结构并给其各元素赋值,然后再将 FS_HEADER 这个数据结构写入到扇区 0 中

  • 将根目录相关信息填充到扇区 1 具体要做的工作是创建一个 FS_ROOT 数据结构并给其各元素赋值,然后再将 FS_ROOT 这个数据结构写入到扇区 1 中

  • 以上这两个初始化工作比较好理解,最后一个扇区分配表的初始化稍微复杂了一点,用语言不容易描述,直接上图理解吧,下图即为扇区分配表初始化后的结果

    扇区分配表初始化

  • 初始化时扇区分配表中的所有管理单元都是空闲的,将这些管理单元挨个串联起来,每一个管理单元都指向下一个管理单元,最后然后再挂到 freeBegin 上形成一个空闲链表

  • 完整格式化代码如下:

    static E_RET FsFormat(void)
    {
        FS_HEADER* header = (FS_HEADER *)FS_MALLOC(SEC_SIZE);  // 申请一个扇区大小的内存用于文件概要信息(扇区 0)数据处理使用
        FS_ROOT* root = (FS_ROOT *)FS_MALLOC(SEC_SIZE);        // 申请一个扇区大小的内存用于根目录信息(扇区 1)数据处理使用
        U32* pUnit = (U32*)FS_MALLOC(SEC_SIZE);                // 申请一个扇区大小的内存用于扇区分配表数据处理
        U32 i = 0;
        U32 j = 0;
        U32 current = 0;
    
        if(NULL == header || NULL == root || NULL == pUnit)
            goto error;
    
        // 初始化扇区 0,即将文件系统概要信息写到扇区 0 中
        StrCpy(header->magic, FS_MAGIC, -1);
        header->sctNum = FS_SEC_NUM;
        // 计算扇区分配表所占扇区数
        // header->sctNum - 2 是因为扇区 0 和 扇区 1 被特定使用,这两个扇区需要先被踢除
        // + !!((header->sctNum - 2) % (SEC_SIZE / 4)) 是因为扇区分配表中最后一个扇区并不一定是满的。不足一个扇区也按一个扇区算
        header->mapNum = (header->sctNum - 2) / (SEC_SIZE / 4) + !!((header->sctNum - 2) % (SEC_SIZE / 4));
        // 格式化时第一个空闲的扇区号即为扇区分配表后的第一个扇区的扇区号
        header->freeBegin = 2 + header->mapNum;
        // 格式化时空闲扇区数等于总扇区数减去扇区 0 和 扇区 1 这两个固定的扇区,然后再减去扇区分配表所占的扇区数
        header->freeNum = header->sctNum - 2 - header->mapNum; 
        // 写到硬盘扇区 0 中
        if(E_ERR == FS_WRITE(0, (U08 *)header))
            goto error;
    
        // 初始化扇区 1,即将根目录相关信息写到扇区 1 中
        StrCpy(root->magic, ROOT_MAGIC, -1);
        // 格式化时根目录下为空,也可以看成是根目录这个文件没有数据,不占用一个扇区
        root->rootBegin = SEC_END_FLAG;
        root->rootNum = 0;
        root->lastBytes = 0;
        // 写到硬盘扇区 1 中
        if(E_ERR == FS_WRITE(1, (U08 *)root))
            goto error;
    
        // 初始化扇区分配表
        for(i = 0; (i < header->mapNum) && (current < header->freeNum); i++)
        {
            for(j = 0; j < MAP_UNIT_NUM; j++)
            {
                *(pUnit+j) = current + 1;
                current++;
                // 如果是最后一个扇区管理单元,则将其内数值设置为结束标志,并且跳出循环
                if(current == header->freeNum)
                {
                    *(pUnit+j) = SEC_END_FLAG;
                    break;
                }
            }
            // 写到硬盘扇区分配表中
            if(E_ERR == FS_WRITE(2+i, (U08 *)pUnit))
                goto error;
        }
        
        FS_FREE(header);
        FS_FREE(root);
        FS_FREE(pUnit);
        return E_OK;
    
    error:
        FS_FREE(header);
        FS_FREE(root);
        FS_FREE(pUnit);
        return E_ERR;
    }
  • 格式化函数实现了之后并不是每次都要被调用的,想一下我们平时使用 U 盘的时候也是只有第一次格式化,之后就再也不需要格式化了

  • 于是再实现一个函数,判断是否格式化过,如果没有格式化则格式化,如果格式化过了,就不再格式化了,函数实现如下,其中添加打印调试信息 printk("format:%d\n", ret);

    E_RET FsIsFormatted(void)
    {
        E_RET ret = E_OK;
        FS_HEADER* header = (FS_HEADER *)FS_MALLOC(SEC_SIZE);  // 申请一个扇区大小的内存用于文件概要信息(扇区 0)数据处理使用
    
        if(NULL == header)
            return E_ERR;
    
        // 读取扇区 0 中的数据,比较 header->magic 中字符串是否等于 FS_MAGIC 
        // 如果相等,则证明硬盘已经被格式化过;如果不相等,则格式化硬盘
        if(E_ERR == FS_READ(0, (U08 *)header))
        {
            FS_FREE(header);
            return E_ERR;
        }
    
        if(FALSE == StrCmp(FS_MAGIC, header->magic, -1))
            ret = FsFormat();
    
        FS_FREE(header);
        return ret;
    }
  • 在测试代码之前需要先提供几个接口函数,需要注意的是因为我们只虚拟化了一块硬盘,该硬盘前面被放了操作系统代码本身,所以我们规划 10M 后的空间用于文件系统

    #define SYS_SEC_NUM         (10*1024*1024/SEC_SIZE)                 // 人为规定硬盘前 10M 空间用于存放系统本身,10M 后才用于文件系统
    #define FS_SEC_NUM          (GetHardDiskSectors() - SYS_SEC_NUM)    // 文件系统管理的硬盘扇区数 = 硬盘总扇区数 - 操作系统本身占用的扇区数
    
    // 定义文件读写底层接口,文件读写以扇区为单位,顾 buf 大小为 SEC_SIZE
    #define FS_WRITE(sn, buf)   HardDiskWrite((sn)+SYS_SEC_NUM, (U08 *)(buf))
    #define FS_READ(sn, buf)    HardDiskRead((sn)+SYS_SEC_NUM, (U08 *)(buf))
    #define FS_MALLOC(size)     Malloc((size))
    #define FS_FREE(p)          FS_Free((p))
  • 在 main 函数中调用 FsIsFormatted,第一次运行结果如下,可以看到格式化成功打印提示信息,退出后,再次运行,不再打印 “format” 字符串

    硬盘格式化测试

  • 本次改动相关代码见:fs.cfs.h

根目录逻辑结构

  • 按照我们平时的使用习惯,格式化完成之后就可以创建文件了并写入数据了,然而现在我们却无法做到,如果此时创建了文件,那么该文件需要以什么样的方式进行组织管理呢?很显然,目前我们的脑海中并没有对应的逻辑设计,换句话说就是,我们虽然有了根目录(root 文件夹),但是该文件夹下的文件具体是如何表示的我们并不清楚

  • 所以接下来我们要做的工作就是搞清楚根目录(root)目录下是的文件是如何组织的

  • 再次强调一个知识点,根目录就是一个文件夹,文件夹的本质是一个特殊的文件,该文件的内容包含了其目录下的其它文件的文件名,文件大小,文件起始扇区等信息,于是我们设计如下的数据结构来描述这些信息

    // 记录一个文件的基本信息
    typedef struct FILE_ENTRY
    {
        U08         name[32];           // 文件名
        U32         fileBegin;          // 文件的第一个扇区号
        U32         fileNum;            // 文件占用的扇区数
        U32         lastBytes;          // 最后一个扇区中的有效数据字节数
        E_F_TYPE    type;               // 文件类型(文件/文件夹)
        U32         inSecIdx;           // FILE_ENTRY 这个数据结构本身也要存储在硬盘上,inSecIdx 为 FILE_ENTRY 这个数据结构本身存储在扇区号
        U32         inSecOff;           // FILE_ENTRY 这个数据结构本身也要存储在硬盘上,inSecOff 为 FILE_ENTRY 这个数据结构本身存储在扇区中的偏移量
        U32         resered[2];         // 保留
    } FILE_ENTRY;
  • FILE_ENTRY 这个数据结构中 inSecIdx 和 inSecOff 这两个元素需要单独说一下,由于 FILE_ENTRY 这个数据结构本身也要存储在硬盘上,inSecIdx 和 inSecOff 这两个元素就是为了找到 FILE_ENTRY 这个数据结构的位置,inSecIdx 为 FILE_ENTRY 所在的扇区,inSecOff 为 FILE_ENTRY 所在扇区中的偏移位置,于是,通过这两个元素就能准确定位出 FILE_ENTRY 这个数据结构具体处在硬盘中的哪个位置

  • 了解了 FILE_ENTRY 这个数据结构之后,根目录的逻辑结构我们就很好理解了

    根目录逻辑结构

  • root 为 FS_ROOT 这个数据结构的实例,表示根目录

  • File A 和 File B 其实就是 FILE_ENTRY 这个数据结构的一个个实例,表示根目录中的一个个文件

  • 从上图可以看到 FS_ROOT.rootBegin 表示 root 到 File A 之间的箭头;FILE_INFO.fileBegin 表示 File A 到文件数据之间的箭头

  • 假如 File A 后面的文件数据发生改变,那么对应的 File A 中的内容也是需要改变的,于是就得知道 File A 在硬盘中的位置,这也就是需要 inSecIdx 和 inSecOff 这两个成员的原因

  • 到这里,根目录的组织结构我们也有了一定的理解,总结一下,首先,我们有 root 这个根目录,其 rootBegin 指向 root 所在的起始扇区,该扇区中存着一个个的 FILE_ENTRY 数据结构,每一个 FILE_ENTRY 数据结构代表着一个文件信息,再通过 fileBegin 找到文件的数据区

扇区的申请与归还

  • 文件系统中一切的操作都要落实到具体的扇区,可以说扇区管理就是实现文件系统的基础。于是,我们进一步思考

    • 如何获取/归还扇区
    • 如何高效的查找扇区
    • 如何为当前文件增加/删除一个扇区
    • ...
  • 先从扇区申请开始,首先在硬盘格式化时候,扇区都是空闲的,它们被依次相连,形成了一个空闲链表,freeBegin 是链表头,在申请扇区时,我们取出 freeBegin 后面的第一个节点(链表头取),为啥是取出第一个节点,因为效率最高呗

    扇区申请示意图

  • 有了申请自然也要有归还,过程与申请完全反过来呗,示意图就不再画了,归还时我们采用链表头插的方式,即插入到 freeBegin 后第一个节点

  • 扇区申请与归还的大方向我们应该已经清楚了,然而距离最终的扇区申请/归还函数实现还有一定的距离

  • 扇区操作是一种外存操作,并不仅仅是在内存中操作链表就可以了,还需要写到外部硬盘中,因此需要仔细计算目标物理位置

  • 扇区管理时使用的是相对扇区地址,扇区读写时使用绝对地址

  • 在扇区分配表中,其内元素是从 0 地址开始,依次往后,其对应着文件数据区也是从 0 开始,依次往后,比如我们想对文件数据区的第 0 号扇区进行读写,调用硬盘读写函数接口时,要写入的扇区号肯定不是 0,我们得找到其对应的实际物理扇区号(绝对地址)

  • 绝对地址与相对地址之间的转换关系:绝对地址 = 相对地址 + mapNum + 2

  • 在已知绝对地址(物理扇区号) sn 的情况下:

    • 计算相对地址:offset = sn - mapNum - 2
    • 计算目标扇区对应的管理单元处在扇区分配表中的第几个扇区:secOff = offset / MAP_UNIT_NUM
    • 计算目标扇区对应的管理单元处在扇区分配表中某一扇区中的偏移量:idxOff = offset % MAP_UNIT_NUM
    • secOff 其实也是相对于扇区分配表所在第一扇区的扇区号,secOff + 2 才是真正的物理扇区号
    • 通过 FS_READ(secOff + 2, (U08 *)pUnit) 将绝对地址(物理扇区号) sn 对应的扇区分配表中的管理单元所在的一个扇区数据读到 pUnit 中,通过 pUnit + idxOff 才能准确访问到物理扇区号 sn 对应的扇区分配表中的管理单元
  • 好了,可以开始写代码了,直接开始写扇区申请函数,想要申请一个空闲扇区,就要找到空闲扇区的链表头,而链表头存在扇区 0 中。于是先申请一个扇区大小的内存空间,用于存放从扇区 0 中读出的信息,ret = header->freeBegin 即返回空闲链表的第一个节点,header->freeNum-- 表示空闲扇区数减 1,这也很好理解。然后空闲链表头 header->freeBegin 需要指向下一个节点,最后再把修改后的 header 写回扇区 0 中

    static U32 AllocSector(void)
    {
        U32 ret = SEC_END_FLAG;
        FS_HEADER* header = (FS_HEADER *)FS_MALLOC(SEC_SIZE);   // 申请一个扇区大小的内存用于文件概要信息(扇区 0)数据处理使用
    
        if(NULL == header || SEC_END_FLAG == header->freeBegin)
            goto error;
    
        if(E_ERR == FS_READ(0, (U08 *)header))                  // 从扇区 0 中读出 header 头信息
            goto error;
    
        ret = header->freeBegin;                                // 返回 freeBegin 链表后的第一个节点
        // header->freeBegin = next;                               // header->freeBegin 指向下一个节点
        header->freeNum--;                                      // 空闲扇区数 - 1
        if(E_ERR == FS_WRITE(0, (U08 *)header))                 // 将 header 头信息写回扇区 0 中
            goto error;
    
        FS_FREE(header);
        return ret;
    
    error:
        FS_FREE(header);
        return SEC_END_FLAG;
    }
  • 我们看到上面的代码中 header->freeBegin = next; 是被注释起来的,原因就是找不到 next 节点

  • 那么我们现在要做的就是找到下一个节点,header->freeBegin 指向的是一个绝对地址(物理扇区号),而我们的目标是要找到其在扇区分配表中的管理单元对应的位置(相对地址),为什么要找这个管理单元位置,因为这个管理单元中存的就是 next 的值,查找 next 节点的代码实现如下,此过程中扇区分配表中的数据也被改了,所以有 FS_WRITE(secOff+2, (U08 *)pUnit) 数据写回扇区分配表的操作

    offset = header->freeBegin - header->mapNum - 2;        // 计算 freeBegin 这个绝对地址(物理扇区号)在扇区分配表中对应的管理单元的相对偏移位置
    secOff = offset / MAP_UNIT_NUM;                         // 计算目标扇区对应的管理单元处在扇区分配表中的第几个扇区,secOff+2 才是其绝对地址(物理扇区号)
    idxOff = offset % MAP_UNIT_NUM;                         // 计算目标扇区对应的管理单元处在扇区分配表中某一扇区中的偏移量
    if(E_ERR == FS_READ(secOff+2, (U08 *)pUnit))            // 读取目标扇区对应的管理单元所处的整个扇区数据
        goto error;
    next = *(pUnit + idxOff);                               // 得到 next 的值
    *(pUnit + idxOff) = SEC_END_FLAG;                       // 由于 (pUnit + idxOff) 这个管理单元对应的扇区被分配出去,所以给其对应的管理单元赋一个无效值
    if(E_ERR == FS_WRITE(secOff+2, (U08 *)pUnit))           // 将 pUnit 数据写回扇区分配表中对应的扇区中
        goto error;
    header->freeBegin = next + 2 + header->mapNum;          // header->freeBegin 指向下一个节点,注意 freeBegin 存的是物理扇区号
  • 有了申请自然也要释放归还,释放动作与申请动作正好是反着来的,传入的扇区号是绝对地址(物理扇区号),跟申请扇区中一样的做法,先通过物理扇区号找到扇区分配表中对应的管理单元,找到了这个管理单元后,把它作为一个链表节点,头插到空闲链表 freeBegin 中,以下为完整的扇区释放代码:

    static E_RET FreeSector(U32 sn)
    {
        FS_HEADER* header = (FS_HEADER *)FS_MALLOC(SEC_SIZE);   // 申请一个扇区大小的内存用于文件概要信息(扇区 0)数据处理使用
        U32 offset = 0;
        U32 idxOff = 0;
        U32 secOff = 0;
        U32* pUnit = (U32 *)FS_MALLOC(SEC_SIZE);                // 申请一个扇区大小的内存用于读取扇区分配表中的一个扇区数据
    
        if(NULL == header || SEC_END_FLAG == header->freeBegin)
            goto error;
    
        if(E_ERR == FS_READ(0, (U08 *)header))                  // 从扇区 0 中读出 header 头信息
            goto error;
    
        offset = header->freeBegin - header->mapNum - 2;        // 计算 freeBegin 这个绝对地址(物理扇区号)在扇区分配表中对应的管理单元的相对偏移位置
        secOff = offset / MAP_UNIT_NUM;                         // 计算目标扇区对应的管理单元处在扇区分配表中的第几个扇区,secOff+2 才是其绝对地址(物理扇区号)
        idxOff = offset % MAP_UNIT_NUM;                         // 计算目标扇区对应的管理单元处在扇区分配表中某一扇区中的偏移量
        if(E_ERR == FS_READ(secOff+2, (U08 *)pUnit))            // 读取目标扇区对应的管理单元所处的整个扇区数据
            goto error;
        *(pUnit + idxOff) = header->freeBegin - 2  - header->mapNum; // 采用头插的方式,新插入的节点指向原头结点;由于扇区分配表中存的是相对地址,故 header->freeBegin 需要转成相对地址
        if(E_ERR == FS_WRITE(secOff+2, (U08 *)pUnit))           // 将 pUnit 数据写回扇区分配表中对应的扇区中
            goto error;
        header->freeBegin = sn;                                 // header->freeBegin 指向新插入的节点
        header->freeNum++;                                      // 空闲扇区数 + 1
        if(E_ERR == FS_WRITE(0, (U08 *)header))                 // 将 header 头信息写回扇区 0 中
            goto error;
    
        FS_FREE(header);
        FS_FREE(pUnit);
        return E_OK;
    
    error:
        FS_FREE(header);
        FS_FREE(pUnit);
        return E_ERR;
    }
  • 完整代码见:fs.c

扇区申请归还测试

  • 申请与归还功能函数已经实现了,肯定是要验证一下看看代码是否有问题

  • 写个测试代码:

    void FsTest(void)
    {
        U32 a[5] = {0};
        FS_HEADER* header = (FS_HEADER *)FS_MALLOC(SEC_SIZE);
        U32 i = 0;
        
        // 1. 读取扇区 0 中的数据到 header 中
        FS_READ(0, (U08 *)header);
    
        // 2. 测试前先打印一下空闲扇区总数
        printk("Free total sectors:%d\n", header->freeNum);
    
        // 3. 连续申请 5 次空闲扇区
        for(i = 0; i < 5; i++)
        {
            a[i] = AllocSector();
        }
        
        // 4. 重新读取扇区 0 中的数据到 header 中,打印此时的空闲扇区数
        FS_READ(0, (U08 *)header);
        printk("After 5 times alloc sectors:%d\n", header->freeNum);
    
        // 5. 连续归还 5 次,打印出每次归还的物理扇区号
        for(i = 0; i < 5; i++)
        {
            printk("a[%d] = %d\n", i, a[i]);
            FreeSector(a[i]);
        }
    
        // 6. 重新读取扇区 0 中的数据到 header 中,打印此时的空闲扇区数
        FS_READ(0, (U08 *)header);
        printk("Free sectors:%d\n", header->freeNum);
    
        FS_FREE(header);
    }
  • 测试代码运行结果如下:

    扇区申请归还测试

  • 从运行结果来看,申请开始前和归还后的空闲扇区总数一致,申请 5 次后空闲扇区数正好也减少 5,说明申请归还功能已经实现成功

  • 注意到两次运行的差异了吗?红色框中的数值是反过来的,这是由于我们在申请和归还空闲扇区的时候操作的时候都是直接操作空闲链表头的,自己可以认真琢磨一下

  • 测试相关完整改动代码见:fs.cmain.c

马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/thin-wind/KOS.git
git@gitee.com:thin-wind/KOS.git
thin-wind
KOS
KOS
main

搜索帮助