简介面向操作系统课程设计的模拟磁盘文件系统项目资源适合正在完成存储管理类课设的本科生参考。项目完整覆盖文件分配表、目录管理、磁盘调度、文件读写与错误恢复等核心模块并通过图形界面直观呈现交互效果便于理解文件系统在磁盘上的组织方式与操作系统的资源管理思路。压缩包共48个文件以Java源码和已编译类为主另有界面布局文件、样式表、截图、Word/PDF文档及演示视频包体仅3.9MB下载与迁移都很方便。文档部分涵盖课程设计报告、指导书和选题注意事项视频展示软件运行效果已有1123人浏览学习过这份资源。对课设答辩、期末复习或希望深入理解磁盘文件系统内部机制的读者这套资料能提供可运行的代码框架与完整的设计分析路径有较高参考价值。1. 模拟磁盘文件系统课设为什么都拿它“开刀”操作系统课设里模拟磁盘文件系统实现是少数“做完能当场演示、答辩能被反复追问”的题目。它不要求你改 Linux 内核而是拿一个普通文件当硬盘把磁盘块怎么分配、目录怎么索引、文件怎么读写的全过程写出来。很多人在这一步卡住不是因为命令记不住而是搞不清超级块、位图、inode 和目录文件之间的关系。下面用一套 Python 结构讲清楚从格式化到挂载、建目录、写文件、删文件的做法适合操作系统慕课版学到一半需要交大作业、或者想拿高分把文件系统原理吃透的同学。2. 用一块普通文件当硬盘模拟磁盘文件系统的块布局与数据结构2.1 为什么要用文件模拟磁盘不是偷懒是为了能随时“开盖检查”如果你做过真实驱动开发就清楚直接对磁盘控制器发指令的课程设计在普通笔记本上几乎没法验收。常见做法是抽象出一层“虚拟磁盘”在一个宿主文件比如vdisk.img里定义块号所有逻辑都通过seek到指定偏移量来读写。这样做的第一个好处是安全就算代码把位图写飞了也只是毁掉一个文件不会把电脑上的重要资料清空第二个好处是可观测你随时可以把vdisk.img的某一段用十六进制编辑器查看玄学问题立刻变成白盒问题。这一步的设计核心是确定两个数字块大小和块数。块大小我一般选 1024 字节也就是 1KB。这个值接近很多简化文件系统的常见选择目录项、inode 和位图都能整块对齐演示算法时也好解释。块数选多大要看你的换算关系虚拟磁盘总容量 块大小 × 块数。如果做 4MB 的磁盘块数就是 4096位图只需要 4096 位也就是 512 字节恰好一个块放得下inode 表如果 32 字节一个一个块能放 32 个 inode32 个块就能放 1024 个 inode。这个容量对课设的几十个文件绰绰有余。创建空磁盘的代码非常直接# make_image.py import os BLOCK_SIZE 1024 BLOCK_NUM 4096 DISK_SIZE BLOCK_SIZE * BLOCK_NUM # 4MB def create_image(path: str) - None: if os.path.exists(path): os.remove(path) # 强制重建避免旧内容干扰 with open(path, wb) as fp: fp.write(b\x00 * DISK_SIZE)这里b\x00 * DISK_SIZE是一次性写入 4MB 的零字节。没有用循环写单个块是因为 Python 对大字节串的连续写入会直接交给底层write效率高很多。后续格式化时会把这些零字节区域按结构切分如果哪一段在你格式化后还是全 0基本可以断定没写到位。这一段的参数值得记一下BLOCK_SIZE1024和BLOCK_NUM4096是整篇代码的“宪法”所有偏移量都由它们推导。不要图省事写死偏移量 2000、3000 这种数字否则后面加一个命令就要重构。常见做法是写一个block_off(block_no)辅助函数所有读写都通过它换算。2.2 四类磁盘结构超级块、位图、inode 表和目录项有了磁盘文件接下来要回答“文件系统怎么知道自己有没有空间、某个文件在哪”。我不建议照搬 ext2 的完整结构课设的目标是能讲清楚不是和真实系统比细节。最常见的简化布局是四段式区域块号范围职责超级块block 0魔数、块大小、块总数、inode 数、空闲块数位图block 1每一位对应一个物理块0 空闲、1 已用inode 表block 2 ~ block 33每个文件一项记录类型、大小、块指针数据区block 34 ~ block 4095存放文件内容、目录文件、间接块超级块是整个文件系统的“黑匣子”入口。它第一个字段必须是魔数。魔数的作用是让挂载程序知道这个文件确实是本文件系统格式而不是随便一个二进制文件。格式化时写入MAGIC bFSIM挂载时先读前 4 个字节不匹配就拒绝。这个习惯很值钱真实文件系统里同样有 magic比如 ext 系列的超块有s_magic。位图的定位是“零号苦力”。它每个位表示一个块是否被占用。由于我们的虚拟磁盘是 4096 块位图只要 512 字节block 1 的一半都用不到剩下部分保持 0。位图怎么更新最容易犯错块 0 到块 33 是系统区格式化时必须把这 34 个位全部置 1数据区从 34 开始才有资格分配。很多初版程序把所有位清 0结果后来分配数据块时把 inode 表覆盖了这是第一节课设翻车点。inode 表里每一项是一个固定大小的结构。为了对齐inode 大小我固定为 32 字节4 字节类型、4 字节大小、4 个直接块指针共 16 字节、1 个一级间接块指针占 4 字节再加 4 字节保留字段。每个 inode 块能放 32 个 inodeinode 区共 32 个块所以最多 1024 个文件。看到这里你应该明白inode 号本质是“第几个 inode 结构”不是文件内容的位置。文件内容位置在 inode 的块指针里。目录项则是另外一层设计目录本身也是一个文件有自己的 inode目录文件的数据区存放的是一串目录项。每个目录项 32 字节其中 4 字节是 inode 号28 字节是文件名UTF-8 编码后截断中文名一般不超过 9 个汉字。目录项的删除不要求复用空间把 inode 号置 0 就算“空槽”查找时跳过 inode 号是 0 的项。这样删除和创建都不需要移动后续数据。2.3 先用 Python 把磁盘结构和常量固定下来代码不是从功能开始的而是先定义结构和打包格式。Python 的struct模块是最适合做二进制协议的工具。每个对象提供一个pack和一个unpack方法写入磁盘前打包读出来再解包。这里唯一要强制的参数是字节序统一用little-endian避免 x86 机器上显示“反过来的数字”。# layout.py import struct BLOCK_SIZE 1024 BLOCK_NUM 4096 INODE_NUM 1024 INODE_TABLE_START 2 DATA_START 34 # inode 表共 32 个块占用 block 2..33 MAGIC bFSIM SUPER_FORMAT 4sIIII # magic, block_size, block_num, inode_num, free_count SUPER_SIZE struct.calcsize(SUPER_FORMAT) # 20 字节 INODE_FORMAT II6I # type, size, d0~d3, indirect, reserved INODE_SIZE struct.calcsize(INODE_FORMAT) # 32 字节 DIRENT_FORMAT I28s # inode 号 文件名 DIRENT_SIZE struct.calcsize(DIRENT_FORMAT) # 32 字节 def block_off(block_no: int) - int: return block_no * BLOCK_SIZE def load_inode(fp, ino: int) - tuple: fp.seek(block_off(INODE_TABLE_START) ino * INODE_SIZE) return struct.unpack(INODE_FORMAT, fp.read(INODE_SIZE)) def save_inode(fp, ino: int, inode: tuple) - None: fp.seek(block_off(INODE_TABLE_START) ino * INODE_SIZE) fp.write(struct.pack(INODE_FORMAT, *inode))这段代码里真正需要注意的不是结构体内容而是“所有偏移都由block_off推导”。load_inode计算的是 inode 表首块偏移加ino * INODE_SIZE如果你漏掉INODE_TABLE_START * BLOCK_SIZE就会把 inode 0 写到 block 0 附近超级块直接被覆盖。这里的struct.calcsize是运行时计算不用手算 20 和 32避免因为格式字符串写错而留下隐患。另外类型字段我约定 0 表示空闲 inode1 表示普通文件2 表示目录。后续所有命令都按这个约定判断。如果你想把文件系统做得更像真实系统还可以在这个基础上扩展权限位、时间戳但课设阶段先把四类结构跑通更重要。3. 格式化与挂载让一张空白磁盘变成可用文件系统的第一关3.1 format 命令写魔数、初始化位图、创建根目录格式化要完成四件事重建超级块、清空位图并把系统区标记为占用、把 inode 0 初始化为根目录、把目录数据清空。注意顺序如果先写 inode 后写超级块中途断电可能留下一个“有 inode 却不知道块大小”的半成品。在课设里format 是可以反复执行的所以最简单的实现就是“删了重来”把磁盘文件重新创建然后从头写。下面这段是格式化核心# ops.py import os, struct from layout import * def init_bitmap(fp): bitmap bytearray(BLOCK_SIZE) for block_no in range(DATA_START): # 系统区全部占用 byte_no, bit_no divmod(block_no, 8) bitmap[byte_no] | (1 bit_no) fp.seek(block_off(1)) fp.write(bitmap) def format_disk(path): if os.path.exists(path): os.remove(path) with open(path, wb) as fp: fp.write(b\x00 * (BLOCK_SIZE * BLOCK_NUM)) # 重建空磁盘 free_blocks BLOCK_NUM - DATA_START with open(path, rb) as fp: fp.seek(block_off(0)) fp.write(struct.pack(SUPER_FORMAT, MAGIC, BLOCK_SIZE, BLOCK_NUM, INODE_NUM, free_blocks)) init_bitmap(fp) root_inode (2, 0, 0, 0, 0, 0, 0, 0) save_inode(fp, 0, root_inode) fp.flush()init_bitmap里的range(DATA_START)只标记 block 0 到 block 33。很多同学觉得系统区也要算“空闲”的于是把所有位清零后面分配数据块时一个不留神就把 inode 表分配出去。其实位图的作用是供“分配器”查询所以系统区必须从一开始就标记为占用空闲块数量也只算数据区即free_blocks BLOCK_NUM - DATA_START 4062。root_inode是 8 个数字对应II6I的 8 个字段类型 2、大小 0、4 个直接块指针都是 0、间接块指针 0、保留字段 0。根目录一开始不预分配数据块等第一次mkdir或touch时再分配。这样格式化逻辑更干净也方便你验证“目录也是文件”。3.2 mount 命令校验魔数并把位图、inode 表加载到内存挂载只需要打开文件、读超级块、校验、加载位图。很多课设把位图每次现读现写这样省内存但很慢而且读错偏移时难排查。我一般习惯在内存里保留一个bytearray所有分配收回操作先改内存需要落盘时再一次性写回位图块。class FS: def __init__(self, fp): self.fp fp self.super None self.bitmap bytearray(BLOCK_SIZE) def mount(path): fp open(path, rb) fp.seek(block_off(0)) data fp.read(SUPER_SIZE) magic, block_size, block_num, inode_num, free_count ( struct.unpack(SUPER_FORMAT, data)) if magic ! MAGIC: fp.close() raise ValueError(不是本文件系统格式请先执行 format 命令) io FS(fp) io.super (block_size, block_num, inode_num, free_count) fp.seek(block_off(1)) io.bitmap bytearray(fp.read(BLOCK_SIZE)) return io挂载打开模式用rb是关键很多人用rb读后面写数据块时报io.UnsupportedOperation: not writable。这里的校验有两层第一层检查magic MAGIC第二层在需要时检查block_size BLOCK_SIZE。如果块大小不一致说明磁盘文件被改动过或者打包时用了不同常量。位图只读一个块就够了。因为 4096 位正好 512 字节bytearray(fp.read(BLOCK_SIZE))会把 1024 字节整块都读进来后半段本来就是 0不影响统计。挂载后不要关闭这个文件句柄整个会话共用它每个命令执行完可以fp.flush()这样直接复制磁盘文件到另一台机器也能正常挂载。3.3 挂载失败的三种表现和调试顺序挂载失败的信息量很大我总结三条血泪经验。第一种魔数不对。先不要改代码用十六进制查看 block 0 前 16 字节看看是不是有 ASCII 乱码。如果显示全是 0说明 format 没真正写进去检查是不是文件句柄打开时被 close 了或者写入后没 flush。第二种魔数对但空闲块数是 0。这不是满盘很可能是位图把系统区之外也置 1 了。格式化时bytearray(BLOCK_SIZE)初始化就是 0只有循环range(DATA_START)置位照理不会出现这种情况真出现就打印位图里统计DATA_START之后有多少个 1。第三种inode 0 类型不是目录。这是 inode 表偏移算错了ino * INODE_SIZE前面少了INODE_TABLE_START * BLOCK_SIZE导致 root inode 写到了 inode 表前面的位置。调试时我习惯在 mount 之后加一条show命令打印超级块字段、位图统计、根目录 inode 内容。这条命令在答辩时也很好用能直接证明“文件系统已经挂载成功”。4. 把读写目录和文件跑通数据块分配、inode 链和目录项操作4.1 目录项查找文件名到 inode 号的“地址翻译”文件系统的路径解析本质就是把目录项数据一块一块读出来拿文件名逐项比对。实现时不要用递归先做单层函数lookup(ino, name)参数 ino 是某个目录文件的 inode 号返回值是子文件 inode 号。def read_block(fp, block_no): fp.seek(block_off(block_no)) return fp.read(BLOCK_SIZE) def read_dir_entries(fs, dir_ino): dir_inode load_inode(fs.fp, dir_ino) entries [] for direct in dir_inode[2:6]: if direct 0: continue data read_block(fs.fp, direct) for off in range(0, BLOCK_SIZE, DIRENT_SIZE): ino, name_raw struct.unpack( DIRENT_FORMAT, data[off:off DIRENT_SIZE]) name name_raw.rstrip(b\x00).decode(utf-8, replace) if ino ! 0: entries.append((ino, name)) return entries def lookup(fs, dir_ino, name): for ino, ent_name in read_dir_entries(fs, dir_ino): if ent_name name: return ino return -1注意load_inode返回的是 tuple索引[2:6]对应 d0~d3因为 tuple 顺序是(type, size, d0, d1, d2, d3, indirect, reserved)。如果你觉得下标太隐晦可以把 inode 解析成一个简单类用inode.direct_blocks这样的属性答辩时更好讲。目录项名字用 28 字节定长保存读取后必须rstrip(b\x00)否则比较a.txt a.txt\x00\x00...永远为 False。这个坑几乎人人会踩一次。如果你在 Windows 上开发的还要注意文件名大小写简单文件系统默认区分大小写README和readme会被当成两个文件答辩前想清楚这个设定。4.2 创建与写入分配 inode 和数据块后如何让目录项落盘创建文件需要三步找一个空闲 inode写入 inode 信息分配数据块并写入内容在父目录文件里添加一个目录项。顺序不能反先分配 inode 再写目录项否则目录项指向一个空的 inode挂载扫描时会出现“悬空项”。分配的核心逻辑def alloc_block(fs): for byte_idx, byte_val in enumerate(fs.bitmap): for bit in range(8): if not (byte_val (1 bit)): block_no byte_idx * 8 bit fs.bitmap[byte_idx] | (1 bit) return block_no return -1 def alloc_inode(fs): for ino in range(1, INODE_NUM): tp, size, *_ load_inode(fs.fp, ino) if tp 0: return ino return -1alloc_block是最容易写错的函数。字节遍历顺序是低位在前所以第 0 块对应 byte0 的 bit0。格式化时已经将系统区置 1这里从数据区之后的块开始找。alloc_inode从 1 开始因为 0 号是根目录不能被覆盖。找到空闲 inode 后要把文件类型和大小先写入再分配数据块。写文件时还需要处理“追加”和“覆盖”的差别。课设里write最简单的语义是覆盖先释放旧块再分配新块。释放旧块意味着把 inode 里所有已用的直接块、间接块都遍历出来把位图对应位置 0。如果你跳过释放直接复用write删除文件时可用空间会莫名减少。创建目录其实和创建文件几乎一样区别在于 inode 类型写 2并且要初始化目录文件的数据块。我第一次实现时只把目录作为“特殊标记”没有给目录分配数据块结果ls永远遍历不到次级目录项后来才意识到目录项必须存在某个文件的数据块里。4.3 读取与删除读回数据块链并把位图块收回读取文件的过程是 inode 索引的逆过程从 inode 拿到直接块指针读对应块如果文件大小超过 4 个直接块能容纳的范围再去间接块里读块号。简化版代码def read_file(fs, ino): tp, size, d0, d1, d2, d3, indirect, _ load_inode(fs.fp, ino) blocks [b for b in (d0, d1, d2, d3) if b] if indirect: fp fs.fp fp.seek(block_off(indirect)) raw fp.read(BLOCK_SIZE) for i in range(BLOCK_SIZE // 4): b struct.unpack_from(I, raw, i * 4)[0] if b: blocks.append(b) data bytearray() for b in blocks: fp fs.fp fp.seek(block_off(b)) data fp.read(BLOCK_SIZE) return bytes(data[:size])这里从间接块读块号时用了unpack_from而不是先struct.unpack(256I, raw)再循环后者也能用但前者更直观而且不会因为块数不满导致 unpack 长度不匹配。直接块只有 4 个单文件最大约 260KB如果希望文件更大把 inode 格式改成 12 个直接块的版本但所有load/save和read_dir_entries的下标都要同步改。删除文件需要三步把 inode 的块指针全部清 0遍历这些块在位图中置 0在父目录项里把 inode 号写 0。顺序上建议先写目录项再释放 inode避免别人通过目录项还能访问到正在释放的文件。释放间接块本身在前间接块里的数据块在后顺序无所谓只要最后位图一致。删除时最容易被忽略的是“目录项空洞”。如果删掉一个目录项不能把后面的项往前移动因为目录文件不是数组而是定长记录流往前移动会破坏正在使用的 inode 号索引。正确做法是把这个 32 字节全部写 0后续read_dir_entries会跳过。5. 操作系统课设避坑模拟磁盘文件系统最常见 5 个翻车点5.1 格式化后重启程序超级块读出来是 0现象执行 format 命令时打印一切正常关闭程序重开mount 报“不是本文件系统格式”。用十六进制工具看vdisk.img的前 16 字节全是 0。原因format_disk里把磁盘文件删掉重建后写入超级块的文件对象没有 flush进程退出时缓冲未落盘或者写入后没有把文件指针归位后续又把其他区域读到 super 位置。解决在format_disk的with open(...)块结束前调用fp.flush()确认落盘调试时在fp.seek(block_off(0))后立刻读回校验。推荐每次写关键区域后调一次flush()代价很小换来的是程序在 IDE 里直接中断重启也不会留下坏盘。5.2 文件超过一个块后后半段读出来是错位现象新建一个 3KB 文件前 1KB 正确后面内容和另一个文件的碎片重叠或者全 0。原因写入时只更新了 inode 的第一个直接块指针第二个和第三个指针还是 0读文件时遇到 0 就跳过于是数据对不上。另一个间接原因是位图分配本身是对的但分配第二个块时从 block 1 开始找把 inode 表区域当数据块分了出去。解决写文件时先调用alloc_block拿到的块号校验它必须大于等于DATA_START再把指针写进 inode 的 d1、d2、d3。调试时打印每次分配到的块号和位图统计连续分配几次看看块号是否在数据区范围内且没有落在系统区。5.3 删除文件后磁盘空闲空间没有增加现象删了一个大文件后再创建同等大小的文件提示没有空闲块或直接报分配失败重新格式化后空间才回来。原因删除函数只是把父目录项里 inode 号改为 0没有把 inode 里的块指针清 0位图中对应位也没有释放。还有可能是删了目录项但没有释放文件 inode 本身导致 inode 泄漏。解决删除时先从 inode 读出全部块号逐个把位图位置 0再把 inode 第 0 个字段类型置 0最后写目录项。顺序上先释放块再释放 inode 更稳妥。释放完成后打印一遍位图统计和文件删除前的统计对比空闲块数应该正好增加文件所占块数。5.4 目录嵌套两层后ls 看不到子目录内容现象在根目录建dir1在dir1里建dir2回到根目录cd dir1后ls看到dir2是空的但再ls dir2又能看到内容。原因目录项查找时用的是“目录文件在哪个 inode 号”的索引但read_dir_entries读目录数据块时把目录文件的直接块指针和根目录的混用了更常见的是创建子目录时给子目录分配了数据块但 inode 的 type 字段写的是文件类型导致读取时按文件内容跳过目录项。解决把“目录也是文件”贯彻到底读取目录一律通过子目录自己的 inode 号避免在路径解析里传父目录 inode。创建目录时 type 写成 2大小写成 0首次写入子目录项时分配一个数据块并更新子目录 inode 的 d0。ls一个目录时先load_inode(dir),再读它的直接块而不是读当前工作目录的块。5.5 内存里明明正常重启进程后报“磁盘结构损坏”现象mount 后连续创建文件、删除、再创建同一会话内都正常进程退出重开再读某个文件报struct.error或目录项乱码。原因你用的是内存位图修改位图后没有写回磁盘或者写回时机只在close时才做而程序异常退出没有触发。真实文件系统把位图视为元数据不是内存临时变量。解决每次alloc_block、free_block之后立即把位图相关字节写回 block 1inode 表同理每次save_inode后马上flush()。不要依赖进程正常退出。另外所有结构统一用I打包关闭程序前把激活的目录项打印一遍能提前发现“半透明”的损坏。6. 让课设拿得出手三个加分设计和一组最少验收脚本6.1 加一个 REPL 命令交互器演示才好看课设验收时比起脚本一次性跑完老师更喜欢你手动输入命令。一个 20 行的 REPL 就能把磁盘变成 shell循环input(fs )把输入按空格 split第一个单词当命令后面当参数。命令表只需要format、mount、ls、mkdir、write、read、rm、exit。每个命令执行后都调一次fp.flush()。这样做还有一个额外好处每敲一条命令都会触发一次落盘等于在做耐久性测试能尽早暴露 5.1 和 5.5 的坑。6.2 加一条 fsck 命令答辩追问有话说加分项建议做fsck或stats命令。fsck做两件事遍历所有 inode把每个文件占用的数据块标记收集起来再和位图比对如果位图标记为已用但在所有 inode 里都找不到对应的块就报告孤儿块。反过来如果 inode 里的块号没有在位图中标记为已用就是缺失分配。这个设计能在答辩时展示你对一致性的理解也能把调试时“看起来正常”的磁盘变成可证明的正确。stats命令就简单些输出总块数、空闲块数、文件数、目录数、位图前几位的十六进制。老师问“你的空闲空间管理怎么看”时直接指位图和统计输出。6.3 准备一条命令回放脚本跑完即验收验收脚本不需要写单元测试框架一个普通文本文件就能完成。把命令按行排列例如format、mkdir dir1、write dir1/a.txt hello、read dir1/a.txt、rm dir1/a.txt。然后让 REPL 支持从文件读输入判断当前行有没有前缀没有就从脚本读有的就从键盘读。这样你提前准备一份 20 行的验收脚本跑完把输出贴到报告里就行。我自己的习惯是每写一个子系统先用十六进制工具把对应 block dump 出来对照设计图检查字段偏移再继续写下一个。这套方法帮我避免了很多“在内存里好端端、落盘就乱”的灵异问题。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?