十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux 二级文件系统课程设计:FAT 块分配与目录检索实战

Linux 二级文件系统课程设计:FAT 块分配与目录检索实战 简介这份 Linux 二级文件系统课程设计文档面向操作系统课程学习者与需要完成综合实验的本科生围绕二级目录结构的磁盘文件系统模拟展开帮助理解文件系统内部功能与实现方法。全文以一份 PDF约 1.35MB共 1 个文件呈现内容完整覆盖实验目的、内容要求、数据结构设计、实现原理以及关键算法流程图。具体实现要求模拟 Login、Dir、Create、Delete、Open、Close、Read、Write 等命令并给出用户登录、注册、目录查看及文件创建删除的 C 语言代码片段涉及 UserMsg 结构体、strcmp 校验、scandir 列目录、creat/open/unlink/mkdir/rmdir 等调用。读者可据此搭建可运行的文件系统原型掌握主目录与子目录的文件形式存储、编号物理地址登记、读写保护等细节并对照流程图与源码梳理排错思路。目前已有 114 人学习下载适合作为课程设计参考与实验报告撰写素材。1. Linux 下的二级文件系统课程设计到底在考什么多数人做完操作系统课程设计才发现评分点从来不是目录树画得像不像而是能不能在一台 Linux 上用用户态程序把磁盘布局、空闲块分配、目录检索这三件事串成一条能跑的链路。二级文件系统特指只支持主目录加用户目录两层结构的文件管理模型路径深度固定为二不处理多层嵌套正是这种约束让它把注意力逼回到块分配与目录查找本身。它模拟的是早期操作系统的文件管理方式边界清晰、规模可控用 500 行左右的 C 代码就能完整闭环。适合正在做操作系统课程设计的学生也适合想补一次底层动手经验的运维和后端开发者在一台装了 gcc 的虚拟机上就能开工。2. 二级文件系统的磁盘布局与核心数据结构2.1 为什么用一个普通文件模拟磁盘在 Linux 上直接操作块设备需要 root 权限写错一个偏移就可能毁掉分区表调试成本极高。课程设计里稳妥的做法是用fopen打开一个大文件当磁盘镜像把字节偏移当成块号来算。Linux 的一切皆文件抽象在这里体现得很直接普通文件的fseek加fread/fwrite语义上和读写磁盘扇区没有区别而普通文件可以随时删除重建回滚只需一条rm。代价是没有缓冲。fread/fwrite走的是标准 C 库的缓冲层一次 512 字节的小块读写会被库里合并行为可预测。如果想让每次写入真的落盘在fopen后加一行setvbuf(fp, NULL, _IONBF, 0)关掉缓冲即可代价是速度变慢但在 512KB 的镜像上完全无感。镜像大小我一般取 1024 块、每块 512 字节合计 512KB。块大小和经典教材保持一致便于对照也方便用od直接看某个块的原始字节。2.2 三段式布局超级块、FAT 与两级目录区磁盘空间划分是所有后续逻辑的地基。常见的划分方式是四段保留引导块、超级块、文件分配表FAT、目录区和数据区。之所以选 FAT 这种显式链接结构而不是位示图加连续分配理由是课程设计里文件大多是被追加写的小文件连续分配要提前预估长度做一次扩容就得整体搬迁FAT 用链式串块追加只需在链尾接一块代价只是随机访问要顺着链跳。物理块区间用途关键说明第 0 块引导块保留不用模拟真实磁盘的引导扇区第 1 块超级块记录魔数、总块数、空闲块数、各区域起始块第 2 至 9 块FAT1024 项每项 4 字节共 4096 字节第 10 至 25 块目录区主目录占 1 块每个用户目录占 1 块第 26 至 1023 块数据区998 块文件内容实际落在这里FAT 表项的取值需要定死否则后面回收空闲块一定会出错。我用的约定是-1表示该块空闲可用-2表示所在文件链到此结束大于等于 0 的整数表示下一块的块号。注意表项下标就是块号所以 FAT 数组自身也要占块第 2 到 9 块在 FAT 里对应的表项要预先标成已占用否则格式化之后第一个被分配出去的块可能就是 FAT 本身。2.3 超级块、目录项与 FAT 的结构体落地结构体定义必须和磁盘上的字节布局严格一致靠的是int在这个平台上固定 4 字节以及结构体成员按声明顺序紧密排列。目录项凑成 32 字节恰好让每块放 16 项除法不会出现余数。#define BLOCK_SIZE 512 #define DISK_BLOCKS 1024 #define NAME_LEN 16 #define DIR_PER_BLOCK (BLOCK_SIZE / 32) #define FAT_FREE (-1) /* 空闲块 */ #define FAT_EOF (-2) /* 文件链末尾 */ /* 超级块固定落在第 1 块 */ typedef struct { int magic; /* 魔数 0x4F534653防止误打开别的文件 */ int total_blocks; /* 总块数校验用 */ int free_blocks; /* 空闲块计数删除时维护 */ int fat_start; /* FAT 起始块号固定 2 */ int dir_start; /* 目录区起始块号固定 10 */ int data_start; /* 数据区起始块号固定 26 */ } super_block; /* 目录项32 字节主目录和用户目录共用同一结构 */ typedef struct { char name[NAME_LEN]; /* 目录项名字不足补 \0 */ int attr; /* 0 普通文件1 目录 */ int start_block; /* 起始块号-1 表示空项 */ int length; /* 文件长度字节 */ int reserved; /* 对齐占位保证 32 字节 */ } dir_entry;attr字段让主目录和用户目录复用一个结构主目录里attr1的项指向某个用户的目录块用户目录里attr0的项指向文件数据。reserved不是可有可无的填充如果省掉它目录项变成 28 字节512 除不尽跨块边界处理会多出一堆特判。3. 在 Linux 上跑通格式化与两级目录切换3.1 主目录与用户目录的语义边界两级目录的核心约束是路径只能写成用户名/文件名没有第三层。主目录固定占第 10 块里面最多放 16 个用户项每项指向一个用户目录块。用户目录块从第 11 块往后依次分配每个用户目录块里最多放 16 个文件项。工作目录这个概念必须显式建模。程序内存里存一个cur_dir_block默认值是第 10 块执行cd 用户名时把它改成该用户对应的目录块号执行cd ..时改回 10。所有文件操作都只看cur_dir_block指向的那一块不解析字符串路径这样就把两级这条规则用数据结构钉死了想支持三层都没有路径可写。3.2 format写超级块、初始化 FAT 和目录区格式化是整个系统里最容易写错的一步因为它同时要写三块区域顺序错了就会出现自相矛盾的状态。稳妥顺序是先算清各区域边界再一次性刷 FAT最后写超级块。static FILE *disk; static int fat[DISK_BLOCKS]; static super_block sb; void write_block(int blk, const void *buf) { fseek(disk, (long)blk * BLOCK_SIZE, SEEK_SET); fwrite(buf, BLOCK_SIZE, 1, disk); } void read_block(int blk, void *buf) { fseek(disk, (long)blk * BLOCK_SIZE, SEEK_SET); fread(buf, BLOCK_SIZE, 1, disk); } int format_disk(void) { char zero[BLOCK_SIZE] {0}; int i; /* 1. 全盘清零保证不存在上一次运行的残留目录项 */ for (i 0; i DISK_BLOCKS; i) write_block(i, zero); /* 2. 初始化 FAT先全部标空闲再占用 0 到 data_start-1 */ for (i 0; i DISK_BLOCKS; i) fat[i] FAT_FREE; for (i 0; i 26; i) /* 引导块 超级块 FAT 目录区 */ fat[i] FAT_EOF; /* 3. FAT 数组本身落盘占第 2 到 9 块 */ fseek(disk, (long)2 * BLOCK_SIZE, SEEK_SET); fwrite(fat, sizeof(int), DISK_BLOCKS, disk); /* 4. 最后写超级块此时镜像已经是自洽状态 */ sb.magic 0x4F534653; sb.total_blocks DISK_BLOCKS; sb.free_blocks DISK_BLOCKS - 26; sb.fat_start 2; sb.dir_start 10; sb.data_start 26; write_block(1, sb); fflush(disk); return 0; }这里有个顺序细节值得说透。如果先写超级块再刷 FAT中间掉电或者程序崩溃镜像就会带着空闲块数 998的超级块和一堆残留 FAT 项下次挂载立刻算错。先清盘、再写 FAT、最后写超级块让超级块成为提交点是文件系统里通用的做法。free_blocks的初值是总块数减 26把第 0 到 25 块全部排除在可分配范围外。fat[i] FAT_EOF而不是FAT_FREE是因为这些块确实已经被占用了标成 EOF 可以让后续的分配器统一跳过。3.3 用 dd、od 和 xxd 验证镜像布局镜像写完不要急着写业务代码先用 Linux 常用命令看一眼原始字节比在 gdb 里断点快得多。虚拟机安装 Linux 之后这些工具默认都有没有的话装bsdmainutils或vim-common即可。# 生成 512KB 的空镜像并运行格式化 dd if/dev/zero ofdisk.img bs512 count1024 ./fs format # 看第 1 块超级块的内部结构按 int 显示前 24 字节 od -A d -t d4 -j 512 -N 24 disk.img # 看第 2 块FAT 起始前 32 项检查 0..25 是否已占用 od -A d -t d4 -j 1024 -N 128 disk.img # 十六进制对照确认魔数 0x4F534653 出现在第 512 字节 xxd -s 512 -l 16 disk.imgod的-j 512定位到第 1 块起点-t d4按 4 字节有符号十进制解释输出的第一个数应该是魔数1330796115也就是0x4F534653。如果这里对不上说明超级块没写到第 1 块后面所有逻辑都不用查了。free_blocks显示 998、fat_start显示 2这两项核对完再往下走。3.4 用户目录的创建与 cd 切换mkdir user1的实现只有三步在cur_dir_block指向的块里找第一个start_block -1的空项从数据区起点的空闲链里取一块作为新用户目录把目录项写回去并同步 FAT。写回去之后别忘了把 FAT 数组整体刷盘因为内存里的fat和磁盘上的副本已经不一致了。int mkdir_user(const char *name) { dir_entry block[DIR_PER_BLOCK]; int newblk alloc_block(); /* 从 FAT 里取一个空闲块 */ if (newblk 0) return -1; read_block(sb.dir_start, block); /* 主目录固定在第 10 块 */ for (int i 0; i DIR_PER_BLOCK; i) { if (block[i].start_block -1) { memset(block[i], 0, sizeof(dir_entry)); strncpy(block[i].name, name, NAME_LEN - 1); block[i].attr 1; block[i].start_block newblk; block[i].length 0; write_block(sb.dir_start, block); sync_fat(); /* 同步 FAT 到第 2 块 */ return newblk; } } free_block(newblk); /* 目录项已满把刚拿的块退回去 */ return -2; }alloc_block在 FAT 里从头扫到第一个值为FAT_FREE的下标找到后立刻标成FAT_EOF并递减free_blocks防止同一块被两次分配。sync_fat负责把整个fat数组写回第 2 块起始位置调用时机是每次分配或回收之后。这两步分开写有个好处批量操作时可以只在结束时同步一次但课程设计规模小我倾向每次改完就同步换来的是崩溃后镜像状态永远可解释。4. 文件创建、读写与删除的完整实现4.1 目录检索按文件名线性扫描目录块二级文件系统里文件数量最多 16 个每用户线性扫描完全够用不需要哈希也不需要 B 树。检索函数是整个模块的公共入口读写删都靠它定位。/* 在当前工作目录中查找名为 name 的项返回槽位下标找不到返回 -1 */ int find_entry(const char *name, dir_entry *out, int *slot) { dir_entry block[DIR_PER_BLOCK]; read_block(cur_dir_block, block); for (int i 0; i DIR_PER_BLOCK; i) { if (block[i].start_block ! -1 strncmp(block[i].name, name, NAME_LEN) 0) { if (out) *out block[i]; if (slot) *slot i; return i; } } return -1; }strncmp而不是strcmp是关键。name字段长 16 字节短文件名后面补的是\0但如果某个文件恰好用满 16 个字符就没有结束符strcmp会越界读到下一个字段。用strncmp限定比较长度长度写NAME_LEN两种情况下都安全。4.2 create 与 write按 FAT 链逐块铺垫文件写入的复杂度全在长度可能跨越多个块这件事上。第一次写要先分配链头链头不足时沿 FAT 跳到链尾再补块。写指针在文件末尾所以追加逻辑天然是连续的。int write_file(const char *name, const char *buf, int len) { dir_entry e; int slot; int first alloc_block(); int cur first, written 0; while (written len) { int chunk len - written; if (chunk BLOCK_SIZE) chunk BLOCK_SIZE; char blk[BLOCK_SIZE] {0}; memcpy(blk, buf written, chunk); write_block(cur, blk); written chunk; if (written len) { int nxt alloc_block(); /* 还没写完链上再接一块 */ if (nxt 0) break; fat[cur] nxt; cur nxt; } else { fat[cur] FAT_EOF; /* 最后一块封链 */ } } sync_fat(); /* 目录项回填找不到同名文件就是新建 */ if (find_entry(name, e, slot) 0) { dir_entry all[DIR_PER_BLOCK]; read_block(cur_dir_block, all); for (int i 0; i DIR_PER_BLOCK; i) { if (all[i].start_block -1) { memset(all[i], 0, sizeof(dir_entry)); strncpy(all[i].name, name, NAME_LEN - 1); all[i].start_block first; all[i].length len; write_block(cur_dir_block, all); return len; } } } return -1; }chunk的计算是唯一容易写错的地方循环里每次最多搬 512 字节len - written小于 512 时就是最后一截直接封链成FAT_EOF。注意fat[cur] nxt必须在下一轮循环写块之前完成否则如果程序在中间崩掉前一块的链指针是空的后一块就成了孤儿块永远回收不了。4.3 read 与 delete遍历链与整链回收读文件按同一套链条走。这里有个隐患如果目录项里记的length和实际链长不一致读操作会读到不属于这个文件的块。所以读完length字节就停用长度而不是FAT_EOF作为终止条件长度是权威来源。场景终止条件依据read 读文件已读字节数 length目录项的 length 字段delete 回收链遇到 FAT_EOFFAT 链指针write 追加本次写入字节数耗尽调用者传入的 len删除比写入简单但必须把整条链上的每一块都标回FAT_FREE漏掉中间一块就是永久泄漏。int delete_file(const char *name) { dir_entry e; int slot; if (find_entry(name, e, slot) 0) return -1; int cur e.start_block; while (cur 0 cur DISK_BLOCKS) { int nxt fat[cur]; /* 先保存下一块再覆盖当前项 */ fat[cur] FAT_FREE; sb.free_blocks; if (nxt FAT_EOF) break; cur nxt; } sync_fat(); write_block(1, sb); /* 空闲块数变了超级块要刷盘 */ /* 目录项清空start_block 置 -1 即视为空槽 */ dir_entry all[DIR_PER_BLOCK]; read_block(cur_dir_block, all); memset(all[slot], 0, sizeof(dir_entry)); all[slot].start_block -1; write_block(cur_dir_block, all); return 0; }顺序上先int nxt fat[cur]再fat[cur] FAT_FREE不能颠倒否则下一跳的块号就被自己抹掉了链会断在半路。回收完链之后必须重写超级块因为free_blocks变了而超级块是判断磁盘是否健康的唯一入口不同步的话下次挂载读到的空闲数还是旧的。4.4 open 表与读写指针的内存模型课程设计通常要求实现open和close本质是维护一张内存打开文件表。表项里存目录项副本、当前读写位置和一个引用计数即可不需要做撤销日志。typedef struct { int in_use; /* 表项是否有效 */ int cur_dir; /* 打开时所在的工作目录块号 */ int slot; /* 目录项槽位下标 */ int offset; /* 读写指针字节偏移 */ dir_entry e; /* 目录项快照 */ } open_file; open_file oft[16]; int fs_open(const char *name) { for (int i 0; i 16; i) { if (oft[i].in_use) continue; if (find_entry(name, oft[i].e, oft[i].slot) 0) return -1; oft[i].in_use 1; oft[i].offset 0; oft[i].cur_dir cur_dir_block; return i; /* 返回文件描述符 */ } return -1; /* 打开表已满 */ }cur_dir要单独存一份因为用户可以在文件打开状态下执行cd ..如果没有快照后续读写会跑到主目录块里去找文件槽位。offset由调用者通过lseek类似的接口修改读的时候从offset对应的块开始取先定位到第offset / BLOCK_SIZE跳再取模算出块内偏移。5. 排错套路与验收演示的加分细节5.1 用 strace 和自检函数定位越界镜像类程序出问题症状往往和病因隔得很远write返回 -1 可能是 FAT 早就错了。第一招是看系统调用确认偏移和长度的计算没跑偏。# 只看文件相关的调用重点核 pwrite 的 offset 是不是块号的 512 倍 strace -e traceread,write,lseek,openat -o fs.log ./fs format # 直接比对第 10 块的主目录项前 16 字节应是用户名 od -A d -c -j 5120 -N 64 disk.img如果lseek的偏移出现 511、513 这种非 512 倍数说明块号是从 1 开始数的而代码里按 0 开始算两边错位一格。这类错误用肉眼比对日志最快。更彻底的办法是写一个自检函数在每次操作后跑一遍把不一致当场暴露出来。它遍历整张 FAT统计FAT_FREE的个数是否等于free_blocks再遍历目录区确认每个目录项的start_block在合法数据区范围内。int check_consistency(void) { int free_cnt 0; for (int i 0; i DISK_BLOCKS; i) if (fat[i] FAT_FREE) free_cnt; if (free_cnt ! sb.free_blocks) { printf(inconsistent: fat%d sb%d\n, free_cnt, sb.free_blocks); return -1; } for (int i 0; i DISK_BLOCKS; i) { int n fat[i]; if (n 0 (n sb.data_start || n DISK_BLOCKS)) { printf(bad link: block %d - %d\n, i, n); return -1; } } return 0; }5.2 常见故障与对应检查点现象大概率原因检查动作format 后 free_blocks 不是 998保留区边界写错看format_disk里的循环上界 26第二个文件覆盖第一个FAT 未同步到第 2 块确认sync_fat在分配后被调用delete 后磁盘占用不降链只回收了头块打印每次cur与nxt的值读到乱码name 字段没补\0用strncmp并检查 memset程序重启后文件消失超级块与 FAT 写入顺序颠倒核对 format 的四步顺序5.3 把验收做成一条可回放的命令答辩时最有说服力的不是当场敲命令而是把整段交互喂进标准输入一次跑完。#!/bin/bash # demo.sh把一串文件系统命令回放进 fs 主程序 cat /tmp/cmd.txt EOF format mkdir alice cd alice create note.txt write note.txt hello-second-level-fs read note.txt ls delete note.txt ls cd .. ls EOF ./fs /tmp/cmd.txt | tee /tmp/out.txt grep -q hello-second-level-fs /tmp/out.txt echo PASS /tmp/cmd.txt把命令文件接进主循环主程序只要用fgets读 stdin 并分发子命令就能配合。输出同时写终端和文件最后用grep -q判断读取内容是否正确一条命令给出 PASS 或 FAIL。把这个脚本和check_consistency的自检输出一起放进演示流程比口头解释布局图更能说明系统的自洽程度。本文还有配套的精品资源点击获取
返回列表