
简介这是一份面向计算机、软件工程等专业学生的操作系统课程设计参考资料聚焦 Linux 环境下二级文件系统的模拟实现适合正在完成操作系统实验、课程设计或准备相关答辩的学习者。文档围绕课程设计目的、内容要求、数据结构设计、实现原理与关键算法流程展开重点讲解 Login、Dir、Create、Delete、Open、Close、Read、Write 等命令的模拟实现并给出主目录、子目录、活动文件等结构组织思路以及用户登录校验、文件创建删除、目录遍历等核心代码片段读者可据此理清二级目录磁盘文件系统的整体框架对照完成编码与调试。资源包共 1 个文件为 1.35MB 的 PDF 文档内容完整、便于打印与查阅。目前已有 114 人学习适合需要快速上手二级文件系统设计、补充实现细节与排错思路的同学参考。1. 二级文件系统到底是什么从磁盘块到用户目录树Linux 下的“二级文件系统”课程设计本质是在用户态把一块固定大小的文件当作“磁盘”自己实现超级块、空闲块管理、inode 和两级目录再对外暴露 create/open/read/write/delete 这组接口。它跟内核里的 ext4、XFS 没有代码关系但你在这套作业里踩过的坑——位图错乱、目录项越界、inode 与数据块不一致——会以另一种形式在内核文件系统里重现。“二级”说的是目录结构只分两层主文件目录MFD记录每个用户名和它的用户文件目录UFD所在块号UFD 中放该用户的文件目录项。适合刚学完操作系统文件管理、需要把“空闲块、inode、目录检索”这些抽象概念落到真实字节偏移上的人。2. 二级文件系统的磁盘布局超级块、位图、inode 怎么排2.1 先定磁盘布局再写 create 和 open如果先动手写 create写到一半会发现找不到“下一个空闲块在哪”的记录点回头改结构体所有偏移常量都得重算。常见做法是把磁盘分成固定几段每段的起始块和用途写进超级块后续读写都通过一个disk_read(blk, buf)统一入口。教学项目里我一般用这套参数每块 512 字节映像文件总共 1024 块也就是 512 KB。布局如下表区域起始块块数用途超级块01全局元数据inode 位图11512 个 inode 的占用位数据块位图221024 个数据块的占用位inode 区464每个 inode 64 字节共 512 个数据区68956文件数据与目录内容提示inode 位图和数据块位图不要合成一个结构。位图错乱时分开看能快速分辨是“目录项挂了”还是“数据块泄漏”。2.2 inode 与目录项的结构体定义结构体一旦发布就尽量别改课程设计后期加字段会连累所有持久化数据的偏移。下面这组字段能覆盖绝大多数验收要求/* fs_struct.h */ #define BLOCK_SIZE 512 #define TOTAL_BLOCKS 1024 #define INODE_NUM 512 #define MAX_BLOCKS 12 /* 直接块指针数量 */ typedef struct { int block_size; int total_blocks; int inode_count; int free_blocks; int free_inodes; int inode_bitmap_start; /* 1 */ int data_bitmap_start; /* 2 */ int inode_start; /* 4 */ int data_start; /* 68 */ } superblock_t; typedef struct { int inode_id; /* 0 号保留1 号为 MFD */ int type; /* 0 空闲 / 1 普通文件 / 2 目录 */ int size; /* 文件字节数 */ int nlink; /* 目录项引用计数 */ int blocks[MAX_BLOCKS]; /* 直接块号-1 表示未分配 */ long ctime; long mtime; } inode_t; typedef struct { int inode_id; /* 0 表示空目录项 */ char name[28]; /* 文件名\0 结尾 */ } dir_entry_t; /* 恰好 32 字节每块 16 项 */dir_entry_t 特意做成 32 字节是为了每块正好放 16 个目录项遍历时不用做除法带偏移算术边界少一半。2.3 MFD 怎么找 UFDUFD 怎么找文件二级结构的关键在于把“用户”和“文件”分成两步查。MFD 独占 inode 1内容是一组 dir_entry每条记录一个用户名和它的 UFD inode 号。UFD 是 type2 的目录 inode内容是该用户的文件目录项。查找/alice/report.txt的流程读 inode 1 的数据块逐个 dir_entry 匹配name alice拿到 UFD 的 inode 号。用该 inode 号载入 UFD遍历它的数据块匹配name report.txt。返回最终 inode 号后续 read/write 都基于这个 inode。因为只有两级路径解析不需要递归栈。如果老师要求“支持三级目录加分”把 UFD 里的 dir_entry 再指向 type2 的 inode 并递归查找即可但很多学校只验收两级先用一级 UFD 跑通再扩展。3. 在 Linux 用户态模拟磁盘块读写与空闲块分配的最小实现3.1 用 fseek fread/fwrite 模拟扇区读写不建议一上来就 mmap因为 mmap 在“磁盘映像持久化”里不好控制刷盘时机。fseek fread/fwrite 更直观/* disk.c */ #include stdio.h #include string.h static FILE *disk_fp NULL; int disk_open(const char *path) { disk_fp fopen(path, rb); if (!disk_fp) { /* 首次运行创建映像并初始化 */ disk_fp fopen(path, wb); if (!disk_fp) return -1; return 1; /* 1 表示新建调用方负责格式化 */ } return 0; } int disk_read(int blk, void *buf) { if (blk 0 || blk TOTAL_BLOCKS) return -1; if (fseek(disk_fp, (long)blk * BLOCK_SIZE, SEEK_SET) ! 0) return -1; size_t n fread(buf, 1, BLOCK_SIZE, disk_fp); if (n 0 ferror(disk_fp)) return -1; /* 真错误 */ if (n BLOCK_SIZE) memset((char*)buf n, 0, BLOCK_SIZE - n); return 0; /* 允许短读用 0 补齐 */ }disk_read里对短读做零填充是关键一步映像刚创建时后面是空洞fread 可能返回 0如果直接当错误处理格式化流程就永远走不下去。disk_open返回 1 是约定告诉上层“这次是新盘先调 format”。3.2 位图法分配与回收一个块的完整函数数据块位图占 2 块共 1024 位每个数据块对应一位。位操作按字节做比先读整块再拆位简单参数值说明映像总大小512 KB1024 × 512 字节位图每字节覆盖8 位对应 8 个数据块数据区起始块68与 superblock.data_start 一致空闲判定位01 表示已占用/* bitmap.c */ static unsigned char bm_buf[2 * BLOCK_SIZE]; /* 缓存数据块位图 */ int bitmap_init(void) { memset(bm_buf, 0, sizeof(bm_buf)); int reserved 68; /* 0~67 已被占用 */ for (int i 0; i reserved; i) bm_buf[i / 8] | (1 (i % 8)); return disk_write(2, bm_buf) || disk_write(3, bm_buf BLOCK_SIZE); } int bitmap_alloc_block(void) { for (int i 68; i TOTAL_BLOCKS; i) { if (!(bm_buf[i / 8] (1 (i % 8)))) { bm_buf[i / 8] | (1 (i % 8)); int bm_blk 2 (i / (BLOCK_SIZE * 8)); disk_write(bm_blk, bm_buf (bm_blk - 2) * BLOCK_SIZE); return i; } } return -1; }位图出错时最常见的表现是“明明删了文件再创建却报磁盘满”多半是 alloc 和 free 没成对或回写位图时块下标算错。调试时用hexdump -C fs.img -s 1024 -n 1024直接看第 2、3 块。3.3 超级块初始化与一致性检查格式化时把超级块写进块 0同时把 inode 位图置零、数据块位图预置保留区int fs_format(const char *path) { superblock_t sb { .block_size BLOCK_SIZE, .total_blocks TOTAL_BLOCKS, .inode_count INODE_NUM, .free_blocks TOTAL_BLOCKS - 68, .free_inodes INODE_NUM, .inode_bitmap_start 1, .data_bitmap_start 2, .inode_start 4, .data_start 68 }; if (disk_write(0, sb) ! 0) return -1; if (bitmap_init() ! 0) return -1; inode_t mfd { .inode_id 1, .type 2, .nlink 1 }; for (int i 0; i MAX_BLOCKS; i) mfd.blocks[i] -1; return inode_write(1, mfd); }注意free_blocks只减不增是新手常见 bug。删除文件时别忘在回收块的分支里同步sb.free_blocks否则磁盘统计会一路飘。4. 二级文件系统的文件操作全链路create 到 delete 的实现与参数4.1 create 与 open目录项插入与 inode 分配create 的实质是两步在目标 UFD 里插入一条 dir_entry再分配一个 inode 并初始化。顺序不能反——先分配 inode 再插目录项中间失败会留下悬挂 inode。int fs_create(const char *user, const char *name, int type) { int ufd_ino mfd_lookup(user); if (ufd_ino 0) return -1; /* 用户不存在 */ inode_t ufd; if (inode_read(ufd_ino, ufd) ! 0) return -1; int slot_blk, slot_off; if (ufd_find_slot(ufd, slot_blk, slot_off) ! 0) return -1; int new_ino inode_alloc(); if (new_ino 0) return -1; inode_t ni {0}; ni.inode_id new_ino; ni.type type; ni.nlink 1; ni.ctime ni.mtime time(NULL); for (int i 0; i MAX_BLOCKS; i) ni.blocks[i] -1; if (inode_write(new_ino, ni) ! 0) return -1; dir_entry_t de { .inode_id new_ino }; strncpy(de.name, name, sizeof(de.name) - 1); if (dir_entry_write(slot_blk, slot_off, de) ! 0) { inode_free(new_ino); /* 失败回滚 */ return -1; } ufd.size sizeof(dir_entry_t); inode_write(ufd_ino, ufd); return new_ino; }参数里type决定普通文件还是目录目录文件的数据块由 create 按需分配普通文件的数据块由 write 触发。open 与 create 的区别是只查找不分配 inode返回一个打开文件表下标后续 read/write 用这个下标索引 offset。open 系统调用背后通常维护一张内存表字段含义fd返回给用户的文件描述符inode_id指向磁盘上的 inodeoffset当前读写位置mode只读 / 只写 / 读写ref引用计数两个用户同时 open 同一个文件各自拿到 fd、彼此独立 offset但共享同一个 inode_id。4.2 read 与 write直接块寻址与块边界read 需要把逻辑偏移映射到具体数据块。因为有 12 个直接块映射逻辑非常直接int fs_read(int ino, char *buf, int len, int offset) { inode_t node; if (inode_read(ino, node) ! 0) return -1; if (offset node.size) return 0; if (offset len node.size) len node.size - offset; int done 0; char blk_buf[BLOCK_SIZE]; while (done len) { int logic (offset done) / BLOCK_SIZE; int inner (offset done) % BLOCK_SIZE; if (logic MAX_BLOCKS) return done; /* 超出直接块范围 */ int blk node.blocks[logic]; if (blk 0) break; /* 读到空洞 */ if (disk_read(blk, blk_buf) ! 0) return -1; int chunk BLOCK_SIZE - inner; if (chunk len - done) chunk len - done; memcpy(buf done, blk_buf inner, chunk); done chunk; } return done; }write 的流程分三种情况逻辑块已分配则读旧块、改区间、回写未分配则先bitmap_alloc_block再把块号登记到inode.blocks[logic]跨多个逻辑块时循环跑前面两步。教程里常犯的错是 offset 超过当前 size 时先手动填零再写其实空洞块不必立即分配读时返回 0 即可这样一段“先读文件末尾、再在很后面写一个字节”的场景不会瞬间浪费掉几个块。提示write 结束前统一更新inode.mtime和size max(size, offsetlen)不要在每个循环里都改一是慢二是中途回滚麻烦。4.3 delete 与一致性回收顺序不能颠倒delete 要做三件事删除 UFD 里的 dir_entry、把 inode 标记为空闲、把 inode 占用的数据块归还位图。三者的顺序直接决定崩溃后能否恢复先清目录项。这样即使后续步骤失败文件也早已不可见。再遍历inode.blocks逐个bitmap_free_block。最后inode_free更新 inode 位图和superblock.free_inodes。如果倒过来先inode_free一个崩溃点就会同时留下“占用中的数据块”和“空目录项”需要写额外的一致性修复逻辑才能对齐。把“先摘目录项”写进 delete 函数顶部的注释下学期重新捡代码时能省一小时。删除目录时还要检查目录内是否还有非空目录项有则返回 ENOTEMPTY不能直接递归清空——这是很多验收必问的点。5. Linux 课程设计验收用 hexdump 和 gdb 定位位图错乱与目录项越界5.1 用 hexdump 看超级块与位图的实际内容格式化后先验证布局别完全相信代码里的偏移常量hexdump -C fs.img -s 0 -n 512 # 看超级块 hexdump -C fs.img -s 512 -n 512 # 看 inode 位图 hexdump -C fs.img -s 1024 -n 1024 # 看数据块位图超级块前 8 个 int 应该依次是 512、1024、512、956、512、1、2、4。如果 block_size 的位置出现 0说明结构体对齐在编译时用了两套规则字段 padding 不一致解决办法是对四个 .c 文件统一加-fpack-struct或直接在结构体前加__attribute__((packed))。数据块位图前 68 位应为 1其余为 0。hexdump 看到第 0、1 字节是 0xFF、第 2 字节是 0x0F 后归零说明保留区正好 68 位、处理正确如果第 4 字节还有非零位多半是数据区起点算错把 inode 区末尾多算进了数据区。5.2 在 create 之后用 gdb 检查 inode 与目录项一致性比满屏 printf 更高效的做法是在 create 返回前下断点直接看内存和磁盘gcc -g -O0 -o fs fs.c disk.c bitmap.c inode.c dir.c gdb ./fs (gdb) break fs_create (gdb) run (gdb) finish # 让 create 执行到底 (gdb) print new_ino (gdb) call inode_read(new_ino, ni) (gdb) print ni.type检查三件事新 inode 的 type 是否为 1、blocks[0]是否 -1文件还没写数据、dir_entry 是否真的写进了 UFD 的某个数据块。如果ni.blocks[0]被意外填了值通常是 inode_alloc 复用了之前没清空的 inode 槽别忘了在 inode_alloc 里 memset 整个 inode 结构。5.3 反复出现的三个边界条件现象常见原因快速定位创建第 17 个文件失败UFD 只分了一个数据块16 项已满在 ufd_find_slot 里加“满则分配新块”分支read 返回 0 但文件非空读到了blocks[logic]0的空洞打印 logic 和 inner确认是否越过了已分配块删除后 free_blocks 不减delete 忘了superblock.free_blocksgrep -n free_blocks *.c对照 write 路径最后提一个容易被忽略的细节目录项里的 name 必须以\0结尾。用 strncpy 后补一行de.name[sizeof(de.name)-1] \0否则 name 长度恰好是 28 时字符串比较会读进紧邻的 inode_id 字段表现出来就是“两个不同文件偶尔被判定为同一个”。本文还有配套的精品资源点击获取