自学内容网 自学内容网

[OS] A4-前菜介绍

从你的描述来看,这段话是给你的一些 预备知识mkfs工具的使用 提示,帮助你了解如何构建和管理文件系统,特别是关于 xv6 文件系统的一些基本操作。

我会通过比喻和通俗化的方式逐步解释:


预备知识:xv6 文件系统的基本结构

  1. 文件系统的构建
    文件系统就像一个巨大的 书库,里面存放着你所有的文件(数据块)。在开始实现文件系统的功能之前,你需要了解这个书库是如何构建的。就像你需要了解书架的设计、每一层存放了多少书、怎么存放书籍,文件系统也是在操作这些基本的“块”(blocks)。

  2. mkfs 程序
    mkfs 程序就像是 构建书架 的工具,它根据指定的参数建立一个文件系统(类似创建一个空的书库)。在 mkfs 执行时,它会给你一些信息,比如文件系统有多少个“书架”(数据块)可以使用。

  3. FSSIZE 参数
    FSSIZE 就是设定书库的 大小,它告诉文件系统最多可以有多少个块(书架)。在你给出的例子中,FSSIZE 被设置为 200,000 个块,这意味着整个文件系统最多可以拥有 200,000 个块来存储数据。

  4. 输出信息: 当你运行 mkfs 时,它会输出文件系统的基本信息,告诉你文件系统的构建情况。例如:

    nmeta 70 (boot, super, log blocks 30 inode blocks 13, bitmap blocks 25) blocks
    199930 total 200000
    

    这里的意思是,文件系统的总共有 200,000 个块,其中:

    • 70 个块用于元数据(这些是描述文件系统本身的数据块)
    • 199,930 个块用于实际存储数据(数据块)

    你可以把这看作是文件系统构建后,书架和书箱的分配情况:70 个书架用于存放“书库”的管理信息,其余的都用来存放实际的书籍(数据)。

  5. 重建文件系统
    如果在开发过程中你需要重新构建文件系统(比如修改了文件系统的设计),你可以运行 make clean,这会删除之前的文件系统,强制重新构建一个新的 fs.img


通俗化解释:

  • 你在做这个作业时,可以把 文件系统 想象成一个书库,每本书就是一个数据块。
  • mkfs 就像是一个工具,帮助你搭建这个书库,它决定书库一共会有多少个书架(块)。
  • 在文件系统创建过程中,FSSIZE 就是控制书库的总容量(有多少个书架)。比如,200,000 块表示书库有 200,000 个“书架”,你可以用它们来存放数据。
  • 元数据 是文件系统本身的管理信息,类似书库的管理者放置在特殊书架上的信息,比如如何分类、哪些书架还空着等。
  • 数据块 就是实际存放文件内容的块,也就是你可以用来存放实际“书”的地方。

通过这样的理解,你会更清楚文件系统构建的基本框架,以及如何使用 mkfs 来创建一个文件系统。


格式指南

项目的结构如下图所示。你也可以使用 ls 命令检查你的目录结构是否正确。目录结构不匹配将导致扣分

对于本次作业,你不需要为附加题(Bonus Part)单独创建文件夹。source 文件夹应包含以下四个文件:

  • fs.c
  • file.h
  • fs.h
  • sysfile.c

如果要提交第 4 次作业的附加题实现,请单独提交以下文件:

  • fs_ec.h
  • fs_ec.c

示例目录结构如下:

main@ubuntu:~/Desktop/Assignment_4_120010001$ ls
Report.pdf  source/
(一个文件夹和一个 PDF 文件)

source 文件夹下:

main@ubuntu:~/Desktop/Assignment_4_120010001/source$ ls
file.h  fs.c  fs.h  sysfile.c
(两个 .c 文件和两个 .h 文件)

请将包含所有所需文件的文件夹压缩成一个 zip 文件,文件名格式应为你的学号,具体如下:

示例文件名:Assignment_4_120010001.zip
报告(Report)应以 PDF 格式提交,并与源代码文件一起包含在压缩包中。格式不符将导致扣分

压缩代码的示例步骤:

main@ubuntu:~/Desktop$ zip -q -r Assignment_4_120010001.zip Assignment_4_120010001
main@ubuntu:~/Desktop$ ls
Assignment_4_120010001                 
Assignment_4_120010001.zip

以下是对这段说明的翻译和整理:


指导说明

  1. 限制范围
    你的实现范围仅限于以下四个文件:

    • fs.c
    • file.h
    • fs.h
    • sysfile.c

    代码需要从标记为 "TODO:" 的注释部分开始实现。

  2. 测试程序入口
    测试程序的入口文件是 bigfile.csymlinktest.c,它们位于 xv6-labs-2022/user 目录下。
    (这是你开始了解测试如何运行的地方。)

  3. 带有 (*) 的部分

    • 这些部分是引导性内容,介绍了工具和函数,帮助你理解系统的功能以及这些组件如何协同工作。
    • 这些部分的代码你需要仔细阅读和理解,但不要修改,除非是标记了 "TODO:" 的部分。
    指导建议:
    1. 先理解引导部分的函数是如何工作的,以及如何使用这些函数。
    2. 在开始实现作业之前,确保你已经对相关内容有了基本的理解。
    3. 我们认为这些引导部分的内容已经足够完成本次作业。
  4. 可选内容(兴趣拓展):

    • 如果你对 xv6 系统感兴趣并希望深入学习,欢迎阅读《xv6-book》以获取更多细节:
      xv6-book PDF
  5. 没有 (*) 的部分

    • 这些部分是需要你实现的 "TODO:" 部分,包含逻辑说明。
    • 在这些部分,你需要根据引导部分介绍的逻辑和提供的 API 来实现函数。
  6. 实现提示

    • 示例代码不会提供
    • 你需要根据引导部分提供的逻辑和 API 自行推导和实现代码功能。

预先准备与设计(吐槽:这部分作业比3050的指导清晰多了)

任务 1:支持大文件


1. 当前 xv6 文件系统的文件大小限制

  • 现状
    • 目前 xv6 文件的最大大小是 268 个块,即 268 × BSIZE 字节BSIZE 在 xv6 中是 1024 字节,即一个块的大小)。
    • 这个限制来源于 xv6 的 inode 结构:
      • 每个 inode 包含 12 个直接块指针1 个单重间接块指针
      • 单重间接块指针 指向一个块,该块中可以存储 256 个数据块的地址
      • 计算文件最大块数:12 + 256 = 268

2. 当前测试程序问题:bigfile 命令

  • 测试失败原因
    • bigfile 命令尝试创建一个 尽可能大的文件 并报告其大小。
    • 提供的模板程序无法写满 256 块,因为测试期望文件能够支持 65803 块
    • 原因是:未修改的 xv6 系统将文件限制在 268 块,而不是测试期望的 65803 块。

3. 目标:实现双重间接块

  • 修改需求

    • 修改 xv6 文件系统,使其支持 双重间接块
    • 每个 inode 添加一个 双重间接块指针。具体逻辑:
      1. 双重间接块指针 指向一个块(双重间接块),该块包含 256 个单重间接块的地址
      2. 每个单重间接块包含 256 个数据块的地址
  • 修改后的最大块数

    • 文件最多可以有:
      256 × 256 + 256 + 11 = 65,803 块
      • 解释:
        • 256 × 256:双重间接块支持的块数(256 个单重间接块,每个单重间接块支持 256 个数据块)。
        • 256:单重间接块本身支持的块数。
        • 11:剩下的直接块(从原来的 12 减去一个用于双重间接块指针)。

4. 可选任务:实现三重间接块(额外加分)

  • 三重间接块的工作方式:

    1. 每个 三重间接块 包含 256 个双重间接块的地址
    2. 每个双重间接块又指向 256 个单重间接块,每个单重间接块再指向 256 个数据块。
    3. 结果是一个文件的块数可以大幅提升,达到: 256 × 256 × 256 + 256 × 256 + 256 + 11
  • 加分实现建议

    • 三重间接块的实现逻辑与双重间接块类似,主要是增加一层指针解析。
    • 对文件系统性能要求更高,可以在完成双重间接块后尝试实现。

总结

  • 目标任务
    • 修改文件系统的 inode 结构,增加对 双重间接块 的支持,从而提升文件系统的最大文件大小到 65803 块。
  • 额外加分任务
    • 在双重间接块的基础上实现 三重间接块,进一步提升文件系统的支持能力。

以下是这段说明的翻译和解析,帮助你理解所需的定义和结构的作用:


定义与说明

阅读参考:
  • 如需详细信息,可参阅《xv6-book》第 8.10 节。
  • 根据以上提示和定义,代码中提供了修改后的结构,请仔细阅读代码中的注释。

修改后的定义(kernel/fs.h 中的宏定义)

  1. 宏定义解释
    • #define NDIRECT 11

      • 直接块的数量从 12 减少到 11
      • 这是因为第 12 个直接块的位置被牺牲,改为存储 双重间接块指针
    • #define NINDIRECT (BSIZE / sizeof(uint))

      • 表示单重间接块中可以存储的块指针数量。
      • 计算公式:BSIZE / sizeof(uint),即 1024 / 4 = 256
    • #define DNINDIRECT (NINDIRECT * NINDIRECT)

      • 表示双重间接块可以存储的数据块数量。
      • 计算公式:256 × 256 = 65536
    • #define MAXFILE (NDIRECT + NINDIRECT + DNINDIRECT)

      • 表示一个文件的最大块数(直接块 + 单重间接块 + 双重间接块)。
      • 计算公式:11 + 256 + 65536 = 65803

修改后的结构(kernel/fs.h 中的 dinode

  1. struct dinode 的定义
    • 作用:表示磁盘上的 inode 结构,包含文件的元信息和块指针。
    • 关键字段解释:
      • short type:文件类型。
      • short major:主设备号(仅适用于设备文件)。
      • short minor:次设备号(仅适用于设备文件)。
      • short nlink:文件链接数(硬链接数量)。
      • uint size:文件的大小(以字节为单位)。
      • uint addrs[NDIRECT + 2]
        • addrs 是块地址数组:
          • 前 11 个地址是直接块指针。
          • 第 12 个地址是单重间接块指针。
          • 第 13 个地址是双重间接块指针(新增)。

修改后的结构(kernel/file.h 中的 inode

  1. struct inode 的定义
    • 作用:表示内存中的 inode 结构,是 dinode 的内存副本。
    • 关键字段解释:
      • uint dev:设备号。
      • uint inum:inode 编号。
      • int ref:引用计数。
      • struct sleeplock lock:锁,用于保护结构中其他字段的并发访问。
      • int valid:是否已从磁盘加载。
      • uint addrs[NDIRECT + 2]
        • dinode 中的 addrs,用于存储直接块、单重间接块和双重间接块的指针。

修改注意事项

  1. 修改说明

    • 结构不可更改:上述的结构(dinodeinode)已提供,请勿修改
    • 仅在 TODO 标记部分实现逻辑。
  2. 关键变化总结

    • 直接块减少到 11:因为要为双重间接块腾出位置。
    • 新增双重间接块支持:在 addrs 的最后一个位置存储双重间接块的指针。

//TODO

 

这一块主要是定义了文件系统中 inode 结构 和一些与块分配相关的宏,用于支持你在作业中实现对大文件的管理。下面是逐步解释,让你更容易理解它们的作用:


什么是 inode?

inode(索引节点)是文件系统中用来描述文件的一个核心数据结构。它不是存储文件内容本身,而是存储文件的元信息,比如:

  • 文件的类型(普通文件、目录、设备文件等)
  • 文件的大小
  • 文件的块地址(指向实际存储数据的位置)

在这个作业中,你要修改 xv6 的 inode 结构以支持 双重间接块,从而使文件系统能管理更大的文件。


宏定义的作用

  1. NDIRECT

    • 表示直接块的数量。
    • 直接块是 inode 中直接指向数据块的指针。
    • 默认情况下是 12,但为了支持双重间接块,作业中改成了 11,把最后一个位置用于双重间接块指针。
  2. NINDIRECT

    • 表示单重间接块中可以存储的块地址数量。
    • 单重间接块是 inode 指向的一个中间块,这个块不存放数据,而是存储其他块的地址。
    • 计算公式是:
      NINDIRECT = BSIZE / sizeof(uint)
      其中 BSIZE = 1024sizeof(uint) = 4,所以 NINDIRECT = 1024 / 4 = 256
  3. DNINDIRECT

    • 表示双重间接块中可以存储的块地址数量。
    • 双重间接块是一个两级指针:
      • 第一层存储指向单重间接块的地址(256 个)。
      • 每个单重间接块又存储 256 个数据块的地址。
      • 计算公式是:
        DNINDIRECT = NINDIRECT * NINDIRECT = 256 × 256 = 65536
  4. MAXFILE

    • 表示一个文件能使用的最大块数,包括:
      • 11 个直接块。
      • 256 个单重间接块。
      • 65536 个双重间接块。
    • 计算公式是:
      MAXFILE = NDIRECT + NINDIRECT + DNINDIRECT = 11 + 256 + 65536 = 65803

dinodeinode 结构

1. dinode(磁盘上的 inode)
  • 作用:这是保存在磁盘上的 inode 结构,记录文件的元信息和块指针。
  • 字段解释
    • uint addrs[NDIRECT + 2]
      • 存储文件块的指针数组:
        • 前 11 个是直接块指针。
        • 第 12 个是单重间接块指针。
        • 第 13 个是双重间接块指针。

2. inode(内存中的 inode)
  • 作用:这是加载到内存中的 inode 结构,用于操作文件时临时存储。
  • 字段解释
    • uint addrs[NDIRECT + 2]
      • 同样存储直接块、单重间接块和双重间接块指针。

总结:这部分的重点

  • 你需要了解的变化

    • NDIRECT 从 12 改为 11,用于给双重间接块腾出空间。
    • addrs[NDIRECT + 2]
      • 现在包含了双重间接块指针(第 13 个位置)。
  • 这些定义和结构不需要修改

    • 它们是预先提供的基础代码,已经为你实现了双重间接块所需的结构调整。
  • 你的任务

    • 在作业中,围绕这些结构实现双重间接块的逻辑,包括数据块分配、释放、读取和写入。

 

块管理相关 API 的详细说明


Block Management(块管理)

文件系统中的块管理是核心部分,用来分配、释放、读取和写入磁盘块。这些 API 主要分为以下几类:

  1. 定义在 fs.c 中的块操作函数
  2. 定义在 bio.c 中的缓冲区操作函数
  3. 定义在 log.c 中的日志记录函数

1. 块管理函数(定义在 fs.c
1.1 bzero(int dev, int bno)
  • 作用:将指定的磁盘块清零(所有内容置为 0)。
  • 参数
    • dev:设备号,表示在哪个设备上执行操作。
    • bno:块号,表示要清零的块。
  • 比喻:这就像你借了一本旧书(磁盘块),需要先擦掉上面的字迹(清零)再开始使用。

1.2 uint balloc(uint dev)
  • 作用:分配一个新的磁盘块,并将其内容清零。
  • 返回值
    • 成功时返回新分配块的块号。
    • 如果磁盘空间不足,返回 0。
  • 比喻:这就像向图书管理员申请一本空白的新书。如果图书馆满了(磁盘空间不足),就无法分配。

1.3 void bfree(int dev, uint b)
  • 作用:释放一个指定的磁盘块,使其可以被其他文件使用。
  • 参数
    • dev:设备号。
    • b:要释放的块号。
  • 比喻:这就像你还书(释放块),让图书管理员把它放回书架,其他人可以借用。

2. 缓冲区管理函数(定义在 bio.c
2.1 struct buf* bread(uint dev, uint blockno)
  • 作用:从磁盘读取指定块到缓冲区,并返回一个已锁定的缓冲区对象。
  • 返回值:返回一个指向缓冲区对象的指针。
  • 比喻:这就像从仓库(磁盘)把一本书拿到借阅区(缓冲区),并锁定它以防止其他人同时访问。

2.2 void brelse(struct buf *b)
  • 作用:释放缓冲区对象,将其移动到最近使用列表的头部,表示可以被其他进程使用。
  • 参数
    • b:指向缓冲区对象的指针。
  • 比喻:这就像你看完书后,把书放回借阅区的书架上,让其他人可以继续借阅。

3. 日志管理函数(定义在 log.c
3.1 void log_write(struct buf *b)
  • 作用:将缓冲区中的数据标记为已修改,并记录日志以便稍后写入磁盘。
  • 典型用法
    1. 调用 bread() 获取缓冲区。
    2. 修改缓冲区中的数据。
    3. 调用 log_write() 记录修改。
    4. 调用 brelse() 释放缓冲区。
  • 比喻:这就像你拿到一本书后,在书上做笔记,然后告诉管理员这些修改需要最终保存到书里。

API 使用场景

这些 API 是实现文件系统操作的基础模块,具体用法如下:

  1. 分配和释放磁盘块

    • 使用 balloc() 分配一个新的磁盘块,并自动清零。
    • 使用 bfree() 释放一个不再需要的磁盘块。
  2. 读取和操作磁盘块

    • 使用 bread() 从磁盘读取块到缓冲区。
    • 修改缓冲区数据后,使用 log_write() 标记为已修改。
    • 使用 brelse() 释放缓冲区。
  3. 清零磁盘块

    • 使用 bzero() 将一个磁盘块的内容清零。

总结:API 的关系与比喻

  • 分配块

    • balloc() 就像借一本新书,同时自动清空书页内容。
    • bfree() 就像还书,让其他人可以使用。
  • 操作块

    • bread() 就像从仓库把书拿到借阅区,锁定它以防止冲突。
    • 修改书后,使用 log_write() 记录你的修改,并稍后保存到仓库。
    • brelse() 就是看完书后,放回借阅区供其他人使用。

下面是包含具体代码的内容以及每行代码的注释和比喻说明:


块管理代码

定义在 fs.c
// 将指定的磁盘块清零(将块内容全部置为 0)
static void bzero(int dev, int bno) {
    struct buf *bp = bread(dev, bno); // 从磁盘读取块 bno 到缓冲区
    memset(bp->data, 0, BSIZE);      // 将缓冲区中的数据清零
    log_write(bp);                   // 标记缓冲区为已修改,记录日志
    brelse(bp);                      // 释放缓冲区
}

逐行说明:

  1. bread(dev, bno):从设备号 dev 中读取块号 bno 的内容到缓冲区 bp
    • 比喻:从仓库(磁盘)取出书(块)到借阅区(缓冲区)。
  2. memset(bp->data, 0, BSIZE):将缓冲区的数据清零。
    • 比喻:把书的内容擦掉,变成一本空白的书。
  3. log_write(bp):记录这次清零操作,稍后写回磁盘。
    • 比喻:告诉管理员这本书已经被清零了,需要保存到仓库。
  4. brelse(bp):释放缓冲区。
    • 比喻:把书放回借阅区的书架上,供其他人使用。

// 分配一个新的磁盘块,并清零其内容
static uint balloc(uint dev) {
    for (int b = 0; b < FSSIZE; b++) {        // 遍历磁盘所有块
        if (block_is_free(dev, b)) {         // 如果块 b 是空闲的
            bzero(dev, b);                   // 将块 b 的内容清零
            mark_block_used(dev, b);         // 将块 b 标记为已使用
            return b;                        // 返回新分配的块号
        }
    }
    return 0;                                // 如果没有可用的块,返回 0
}

逐行说明:

  1. 遍历块:通过循环遍历磁盘中的每个块。
    • 比喻:管理员在整个仓库中寻找空白的书架。
  2. block_is_free(dev, b):检查块是否是空闲的。
    • 比喻:检查这个书架是否没有放书。
  3. bzero(dev, b):将空闲块的内容清零。
    • 比喻:如果书架是空的,先清空上面的灰尘。
  4. mark_block_used(dev, b):将块标记为已使用。
    • 比喻:在记录本上标注这本书(块)已经借出去。
  5. 返回块号:如果找到空闲块,返回其编号。

// 释放一个磁盘块
static void bfree(int dev, uint b) {
    clear_block_usage(dev, b);   // 将块 b 标记为未使用
}

逐行说明:

  1. clear_block_usage(dev, b):将块号 b 标记为未使用。
    • 比喻:把书架上的书还给图书馆,标记这个书架可以再次使用。

定义在 bio.c
// 从磁盘读取指定块到缓冲区,并返回缓冲区对象
struct buf* bread(uint dev, uint blockno) {
    struct buf *bp = buffer_cache_find(dev, blockno); // 在缓存中查找块
    if (!bp) {                                      // 如果缓存中没有
        bp = buffer_cache_allocate(dev, blockno);   // 分配一个新的缓冲区
        disk_read(dev, blockno, bp->data);          // 从磁盘读取块数据到缓冲区
    }
    lock_buffer(bp);                                // 锁定缓冲区,防止其他进程访问
    return bp;                                      // 返回缓冲区
}

逐行说明:

  1. buffer_cache_find(dev, blockno):先查找缓存中是否已有该块。
    • 比喻:管理员先在借阅区(缓存)找这本书,看是否已经有人借出。
  2. buffer_cache_allocate(dev, blockno):如果缓存中没有,则分配一个新的缓冲区。
    • 比喻:如果书不在借阅区,就分配一个新空位来存储这本书。
  3. disk_read(dev, blockno, bp->data):从磁盘读取块号为 blockno 的数据到缓冲区。
    • 比喻:从仓库(磁盘)搬运书到借阅区。
  4. lock_buffer(bp):锁定缓冲区,防止其他进程访问。
    • 比喻:管理员锁定书本,防止被多个人同时借走。

// 释放缓冲区
void brelse(struct buf *b) {
    unlock_buffer(b);          // 解锁缓冲区
    move_to_recent_list(b);    // 将缓冲区放到最近使用列表的头部
}

逐行说明:

  1. unlock_buffer(b):解锁缓冲区。
    • 比喻:管理员解锁书本,允许其他人查看。
  2. move_to_recent_list(b):将缓冲区移到最近使用列表头部。
    • 比喻:把书放到书架前排,方便下次快速找到。

定义在 log.c
// 记录缓冲区的修改,稍后写入磁盘
void log_write(struct buf *b) {
    pin_buffer(b);             // 增加缓冲区的引用计数,防止被替换
    mark_buffer_dirty(b);      // 标记缓冲区为已修改
    log_commit_buffer(b);      // 将修改记录到日志中
}

逐行说明:

  1. pin_buffer(b):增加引用计数,防止缓冲区被替换。
    • 比喻:管理员标注这本书暂时不能借出,还在修改中。
  2. mark_buffer_dirty(b):标记缓冲区内容为脏数据,表示需要写回磁盘。
    • 比喻:书已经被修改,管理员需要保存笔记。
  3. log_commit_buffer(b):将缓冲区的修改记录到日志。
    • 比喻:管理员记录修改内容,稍后存档。

总结

  • 这些 API 构成了文件系统中 磁盘块管理和缓存机制 的基础逻辑。
  • 它们的主要功能是:
    1. 分配、释放块fs.c):申请和回收磁盘上的存储空间。
    2. 缓存管理bio.c):从磁盘读取数据到内存,并提供缓存机制。
    3. 日志记录log.c):跟踪缓冲区的修改,确保数据一致性。

 


原文地址:https://blog.csdn.net/m0_74331272/article/details/144144244

免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!