C++实现二级文件系统:MFD/UFD目录结构与磁盘管理
发布时间:2026/9/16 20:58:14 作者:尧图编辑部 阅读量:1,286

简介面向操作系统课程文件系统专题这是一份完整的二级文件系统实验资源包含基于QT的图形界面工程与原始控制台源码解决多用户文件系统设计中的目录结构、文件读写及权限保护等核心问题。资源共25个文件以cpp、h、ui源码和pro工程文件为主附带doc/docx实验报告文档压缩包仅1.64MB结构清晰便于直接打开工程运行或对照源码学习。已有663人学习下载。代码实现了login、dir、create、delete、open、close、read、write等命令列目录时输出文件名、物理地址、保护码和文件长度主目录与子目录均以文件形式存放磁盘并通过编号映射物理地址。实验报告与源码注释有助于理解文件系统内部机制适合高校学生、课程设计者及Linux文件系统初学者参考和二次开发。1. 二级文件系统到底在解决什么问题如果你只在 Linux 上敲过 ls、cat、mkdir很难想象这些命令背后的目录树是怎么组织起来的。这个资源给出的答案是用 C 手工搭一个二级文件系统——主目录 MFD 下挂若干用户子目录 UFD每个用户目录里的目录项记录文件名、物理地址、保护码和长度文件内容不直接塞进内存而是按编号落到“磁盘”上的 file0、file1……。实践者会以 login 开头敲完 dir、create、write 再 logout在控制台里完整走一遍文件生命周期。对正在做操作系统课程设计的人、想深入 Linux 文件系统但没有时间读内核源码的工程师、以及准备面试时被问“文件系统的工作原理”的人来说这份资源比只看 PPT 强在两点一是逻辑全部落在 C 源码里二是 Qt 界面让过程可视化。拿到它之后你不需要再补教材里那些空泛的概念直接拆代码就行。2. 数据结构与磁盘布局MFD、UFD 与 FCB 怎么落盘2.1 主目录 MFD 和用户子目录 UFD 的分工二级文件系统的“二级”指目录树只有两层顶层是主目录 MFD底层是每个用户各自的用户文件目录 UFD。登录时login 命令先查 MFD确认用户名和密码后把当前用户切换到对应的 UFD之后 dir、create、delete、read、write 操作都只能在这张 UFD 里进行。这样设计的好处是用户之间的文件相互隔离权限判断点集中在两处登录时看 MFD访问文件时看 UFD 的目录项。注意实验提示里的这句话“主目录和子目录都以文件的形式存放于磁盘”。也就是说 MFD 和 UFD 不能只在内存里维护一份必须落盘。常见做法是在模拟磁盘目录下放一个 mfd.dat 保存用户表每个用户再对应一个 ufd_0.dat、ufd_1.dat……登录时加载当前用户的 UFD登出或每次修改后写回。要验证是否真的落盘退出程序再启动执行 dir如果文件还在就说明目录项写回来了。2.2 目录项字段设计文件名、物理地址、保护码、长度源码里文件系统相关的结构体通常集中在 Mainfun.h 中由主程序 include 后使用。目录项可以理解成简化版 FCB至少包含用户名、文件标识、文件名、物理地址、保护码、长度和有效标记// Mainfun.h 中常见的设计实际字段以源码为准 #define MAX_NAME 16 #define MAX_FILE 32 struct DirEntry { // UFD 中的一个目录项 char username[MAX_NAME]; // 文件所有者 int fileid; // 文件标识号从 1 开始递增 char filename[MAX_NAME]; // 文件名不含路径 int physblock; // 物理地址存放 file0~fileN 的编号 int protect; // 保护码0读写 1只读 2只写 3禁止访问 int length; // 文件长度字节 bool used; // 是否已分配 };这里有几个设计取舍值得关注。fileid用于标识同一个用户的不同文件即使两个用户都创建了同名文件fileid 也不同physblock不是内存地址而是模拟磁盘的逻辑块号protect是权限位write/delete 时先看它open 时还要再看打开模式。把目录项写成这种扁平结构dir 命令执行时只需要遍历一张 UFD 表。字段类型含义create 时怎么赋值usernamechar[16]属于哪个用户从当前登录用户拷贝fileidint文件标识号取当前 UFD 最大 fileid 1filenamechar[16]文件名由 create 命令参数写入physblockint物理地址分配一个空闲块号对应磁盘文件 fileNprotectint保护码默认 0可读可写可随后修改lengthint文件长度初始 0write 后累加usedbool槽位是否占用create 分配delete 置 false实验要求“列目录时列出文件名、物理地址、保护码和文件长度”说明这套目录项已经覆盖了你要展示的全部信息。UI 上哪怕不画图形化列表直接把这些字段按列打印也是一个合格的实验交付。2.3 用 file0、file1 模拟磁盘块物理地址登记策略“物理地址”在这个实验里不是扇区号而是逻辑块编号。目录项里的 physblock2表示文件内容写到磁盘目录下的 file2 中。空闲块管理可以简单用一个 bool 数组记录哪些编号被占create 时顺序找最小空闲编号delete 时把对应编号置为空闲。这样不需要真的实现磁盘驱动也能让“物理地址”字段有实际意义。需要考虑的一点是file0 不要分配给普通用户文件留作 MFD 或超级块镜像UFD 的镜像从 file1 开始排。否则登录时盘上还没有统一格式逻辑会很乱。实际操作中我会单独建一个 disk 子目录所有模拟磁盘文件都放进去理论上 MFD、UFD 和数据文件都在这一个目录下方便查看也方便一次性清理重来。文件长度也不要等于物理块大小write 只追加在 length 处所以磁盘上的物理文件会越写越大这是正常现象。3. Qt 界面与原始控制台源码双实现如何共用一套核心逻辑3.1 控制台程序的命令解析流程原始控制台源码并不复杂。main.cpp 里通常是一个 while 循环每次从 stdin 读一行用 sscanf 切出命令和参数再通过一组 strcmp 找到对应处理函数。下面这段是这种分发模型的简化版// 控制台版入口读一行命令切分出 cmd 和 arg char line[128]; while (fgets(line, sizeof(line), stdin) ! nullptr) { line[strcspn(line, \n)] 0; // 去掉结尾换行 char cmd[16] {0}, arg[64] {0}; sscanf(line, %15s %63s, cmd, arg); // 限制宽度防止溢出 if (strcmp(cmd, login) 0) doLogin(arg); else if (strcmp(cmd, dir) 0) doDir(); else if (strcmp(cmd, create) 0) doCreate(arg); else if (strcmp(cmd, delete) 0) doDelete(arg); else if (strcmp(cmd, open) 0) doOpen(arg); else if (strcmp(cmd, close) 0) doClose(arg); else if (strcmp(cmd, read) 0) doRead(arg); else if (strcmp(cmd, write) 0) doWrite(arg); else if (strcmp(cmd, logout) 0) break; else fprintf(stderr, unknown command: %s\n, cmd); }这套代码的价值在于把命令语义和界面彻底分开。doCreate 只关心文件名是否存在、目录项是否还有空位不关心输入来自键盘还是文本框。sscanf 的%15s和%63s限制了最大宽度防止超长输入写坏缓冲区如果你把源码里的 char 数组换成 std::string解析部分还需要进一步处理空格和中文文件名。3.2 Qt 界面的事件驱动与对话框Qt 工程没有把命令逻辑重写一遍而是让按钮槽函数直接调用控制台源码里的 doCreate、doDelete 等函数。整个工程的 mainwindow.ui、logindialog.ui、writedialog.ui、readdialog.ui、createfile.ui 分别对应登录、写文件、读文件、创建文件这几个交互场景。// MainWindow.cpp创建按钮槽函数复用控制台的 doCreate void MainWindow::onCreateAction() { QString name ui-nameEdit-text().trimmed(); if (name.isEmpty()) { ui-consoleEdit-append(tr(create: 文件名不能为空)); return; } // 控制台函数接收 const char*这里把 QString 转成本地编码 int ret doCreate(name.toLocal8Bit().constData()); if (ret 0) ui-consoleEdit-append(tr(create %1 ok).arg(name)); else ui-consoleEdit-append(tr(create failed, ret%1).arg(ret)); reloadDirTable(); // 刷新界面上的目录表格 }这里的核心是把返回值用起来。doCreate 返回 0 表示成功负数表示失败界面只负责根据返回值提示用户。toLocal8Bit 在 Linux 下通常是 UTF-8在 Windows 下是 GBK如果你的源码里对中文文件名有编码要求这里需要统一否则同一份源码编译出的界面版本可能出现“文件创建成功但 dir 显示乱码”的怪问题。3.3 在 Qt 工程里保留原始控制台源码的价值文件包里 main.cpp、Mainfun.cpp 和 Qt 的 main window 代码同时存在filesystem.pro 把它们编进同一个工程。这不是代码冗余而是调试成本最低的路径。Qt 界面一旦卡在某条命令上不需要猜界面状态直接切到终端跑同样的命令序列看返回码就能定位问题是命令实现本身还是界面参数传递错误。我在自己拆这类工程时习惯先跑控制台版本login、create、open、write、close、delete 各敲一遍确认命令层没问题再打开 Qt 界面复现同样步骤。课程设计里最容易被坑的是 write 命令Qt 的 QTextEdit 会把换行符保存为 \n但某些版本的 QPlainTextEdit 在 Windows 上会带回 \r\n如果 doWrite 里没有对长度做特殊处理文件长度和 read 出来的内容就会对不上。保留控制台源码后这个问题 5 分钟内就能定位。4. 读写保护与文件操作从 open 到 write 的权限校验4.1 保护码怎么定义才能兼容控制台和 Qt 两套入口保护码的取值没有统一标准课程设计里最常见的是 0~3 四个档位。为避免魔法数字散落在代码里建议在 Mainfun.h 中定义成宏#define PROT_RDWR 0 // 可读可写 #define PROT_READ 1 // 只读 #define PROT_WRITE 2 // 只写 #define PROT_NONE 3 // 禁止访问保护码含义readwritedelete0可读可写允许允许允许1只读允许拒绝拒绝2只写拒绝允许拒绝3禁止访问拒绝拒绝拒绝注意 delete 是否受保护码约束实验题目没有明确要求。我倾向于让 delete 也受保护避免用户用一个只读文件被自己误删但如果你希望 delete 只受登录用户身份限制可以把表格里的 delete 列全部改成允许两种做法都不会影响课程验收只要在实验报告里写明即可。4.2 create、open、write 的调用链一个文件的完整生命周期是 create - open - write/read - close - delete。create 只负责分配目录项并创建模拟磁盘文件此时文件还没有进入活动状态open 把目录项的信息登记到活动文件表并记录打开方式write 再次校验保护码和打开方式确认后才写入数据// Mainfun.cpp 中 doWrite 的核心逻辑 int doWrite(const char* fname, const char* buf, int len) { int idx findFile(fname); if (idx 0) return -1; // 文件不存在 // 第一道检查保护码允许写 if (ufd.dir[idx].protect ! PROT_RDWR ufd.dir[idx].protect ! PROT_WRITE) { fprintf(stderr, write denied: protect%d\n, ufd.dir[idx].protect); return -2; } // 第二道检查打开方式允许写 if (oft.items[currentFD].mode ! OPEN_WRITE oft.items[currentFD].mode ! OPEN_RDWR) { fprintf(stderr, write denied: file not opened for write\n); return -3; } // 从当前 length 处开始追加写 char path[32]; snprintf(path, sizeof(path), disk/file%d, ufd.dir[idx].physblock); FILE* fp fopen(path, rb); fseek(fp, ufd.dir[idx].length, SEEK_SET); fwrite(buf, 1, len, fp); ufd.dir[idx].length len; // 更新元数据 fclose(fp); saveUFD(); // 写回目录项 return len; }这里有两道权限门槛。第一道是保护码表示这个文件天然允许哪些操作第二道是打开模式表示这次会话里进程申请了哪种访问权。即使保护码允许写如果 open 时只传入 OPEN_READwrite 也要拒绝。真实文件系统里同样是这个逻辑打开时确认访问权限读写时检查文件状态。physblock 在目录项里存的是编号真正读写时拼出disk/fileN路径这样就把虚拟块号和物理文件对应起来了。saveUFD 这一步容易被忽略。如果不把更新后的 length 写回磁盘进程退出后目录项里长度还是旧值read 要么读不全要么多读出一段脏数据。我在写这类程序时会专门在 doClose 和 doDelete 的返回值里带上元数据回写状态方便确认。4.3 一条可复现的完整操作序列建议按下面的顺序跑一遍每一步关注预期的输出login user1系统加载 user1 的 UFD提示登录成功。create demo.txtUFD 中新增一个目录项磁盘上出现 fileN。open demo.txt w活动文件表登记一条记录模式为写。write demo.txt hello写入 5 个字节length 变为 5。close demo.txt释放活动文件表条目。open demo.txt r以只读方式打开。read demo.txt读到 hello。delete demo.txt目录项 used 置 false并把对应物理块回收。这个序列覆盖了文件系统的核心闭环create 建元数据open 建立访问通道write 改数据close 释放通道delete 回收空间。每次改动后都执行 dir观察文件名、物理地址、保护码和长度四列的变化能很快定位是目录项还是数据文件出了问题。5. 实用技巧用命令函数表把 Qt 槽函数瘦成一层壳5.1 命令函数表控制台源码里那串 strcmp 链在界面里很容易膨胀每个按钮一个槽槽里再做 if 判断。更好的做法是建一张命令函数表让 Qt 输入框和按钮都走同一个 dispatch// 统一命令函数签名目录项和活动文件表作为全局状态 typedef int (*CommandHandler)(int argc, char* argv[]); struct CommandEntry { const char* name; CommandHandler handler; }; CommandEntry cmdTable[] { {login, cmdLogin}, {dir, cmdDir}, {create, cmdCreate}, {delete, cmdDelete}, {open, cmdOpen}, {close, cmdClose}, {read, cmdRead}, {write, cmdWrite}, };// MainWindow.cpp把输入行直接交给命令表执行 int dispatch(const QString raw) { QStringList parts raw.split( , QString::SkipEmptyParts); if (parts.isEmpty()) return -1; QByteArray bufs[8]; char* argv[8] { nullptr }; int argc qMin(parts.size(), 8); for (int i 0; i argc; i) { bufs[i] parts[i].toLocal8Bit(); argv[i] bufs[i].data(); } for (const CommandEntry e : cmdTable) { if (parts[0].compare(e.name, Qt::CaseInsensitive) 0) { return e.handler(argc, argv); } } return -2; // unknown command }QByteArray 数组持有每个参数的内存argv 里的指针指向这些数据函数执行期间有效。这样做的收益是界面上加一个命令输入框回车调用 dispatch下面的按钮也调用 dispatch两套入口共享同一份命令解析和错误处理不会再出现“按钮传了文件名、控制台传了路径”的分叉。5.2 UI 线程同步调用 vs 工作线程这个实验的文件操作都是小文件读写几千字节耗费不到一毫秒直接在 UI 线程里同步调用没问题。唯一需要留意的场景是 dir 命令如果磁盘目录下积累了成百上千个 fileN界面可能卡顿。想稳妥一点就把 dispatch 放到 QtConcurrent::run 里结果用信号传回但课程设计阶段同步调用完全够用还能避免一批 QObject 生命周期问题。5.3 验证文件系统是否真的落盘的检查点最后给你几个自查项退出程序后重启login 再 dir 能看到之前的文件protect 为 1 的文件 write 必须返回失败read 读出的字节数和 length 一致delete 后再 create 一个文件能复用之前的物理块编号。把这些场景写成一个脚本化清单比对着验收表逐条勾选快得多。我的习惯是在 logout 的槽函数里加一行 qDebug打印本次会话共回写了几块 UFD 元数据这样每次退出时就能直观确认目录项有没有真正写到磁盘文件里。本文还有配套的精品资源点击获取