news 2026/9/16 20:58:10

C++实现二级文件系统:MFD/UFD目录结构与磁盘管理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++实现二级文件系统:MFD/UFD目录结构与磁盘管理

简介:面向操作系统课程文件系统专题,这是一份完整的二级文件系统实验资源,包含基于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 + 1
filenamechar[16]文件名由 create 命令参数写入
physblockint物理地址分配一个空闲块号,对应磁盘文件 fileN
protectint保护码默认 0(可读可写),可随后修改
lengthint文件长度初始 0,write 后累加
usedbool槽位是否占用create 分配,delete 置 false

实验要求“列目录时列出文件名、物理地址、保护码和文件长度”,说明这套目录项已经覆盖了你要展示的全部信息。UI 上哪怕不画图形化列表,直接把这些字段按列打印,也是一个合格的实验交付。

2.3 用 file0、file1 模拟磁盘块:物理地址登记策略

“物理地址”在这个实验里不是扇区号,而是逻辑块编号。目录项里的 physblock=2,表示文件内容写到磁盘目录下的 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 // 禁止访问
保护码含义readwritedelete
0可读可写允许允许允许
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, "r+b"); 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_READ,write 也要拒绝。真实文件系统里同样是这个逻辑:打开时确认访问权限,读写时检查文件状态。physblock 在目录项里存的是编号,真正读写时拼出disk/fileN路径,这样就把虚拟块号和物理文件对应起来了。

saveUFD 这一步容易被忽略。如果不把更新后的 length 写回磁盘,进程退出后目录项里长度还是旧值,read 要么读不全,要么多读出一段脏数据。我在写这类程序时会专门在 doClose 和 doDelete 的返回值里带上元数据回写状态,方便确认。

4.3 一条可复现的完整操作序列

建议按下面的顺序跑一遍,每一步关注预期的输出:

  1. login user1:系统加载 user1 的 UFD,提示登录成功。
  2. create demo.txt:UFD 中新增一个目录项,磁盘上出现 fileN。
  3. open demo.txt w:活动文件表登记一条记录,模式为写。
  4. write demo.txt hello:写入 5 个字节,length 变为 5。
  5. close demo.txt:释放活动文件表条目。
  6. open demo.txt r:以只读方式打开。
  7. read demo.txt:读到 hello。
  8. 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 元数据,这样每次退出时就能直观确认目录项有没有真正写到磁盘文件里。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 20:57:25

MCP23S17与STC12C5A32S2硬件SPI扩展16路IO实战

简介&#xff1a;一套以STC12C5A32S2为主控、通过SPI协议驱动MCP23S17扩展I/O引脚的嵌入式开发工程包&#xff0c;适合正在学习8051单片机、SPI通信及并行I/O扩展的开发者参考。工程文件完整&#xff0c;共23个文件&#xff0c;包含MCP23S17的C语言驱动源码、引脚定义头文件、U…

作者头像 李华
网站建设 2026/9/16 20:57:06

DGX Spark开发环境配置与优化指南:个人AI超级计算机实战

拿到DGX Spark之后&#xff0c;我才意识到“个人AI超级计算机”这个词的含金量。这台只有小主机体积的设备&#xff0c;塞进了NVIDIA号称能提供1 PFLOP算力的GB10超级芯片&#xff0c;再加上128GB统一内存&#xff0c;意味着我可以直接在桌面机上跑百亿甚至千亿参数的大模型微调…

作者头像 李华
网站建设 2026/9/16 20:53:42

OptiScaler:老游戏跑FSR4帧生成?一份完整上手指南

OptiScaler&#xff1a;老游戏跑FSR4帧生成&#xff1f;一份完整上手指南 【免费下载链接】OptiScaler OptiScaler bridges upscaling/frame gen across GPUs. Supports DLSS2/XeSS/FSR2 inputs, replaces native upscalers, enables FSR-FG/XeFG on non-FG titles. Supports N…

作者头像 李华
网站建设 2026/9/16 20:52:40

Python处理ZIP压缩包:发电量数据解压与编码修复全指南

简介&#xff1a;面向能源经济、区域发展、电力规划等领域研究者的省级发电量面板数据&#xff0c;覆盖2005至2022年间各省份发电量情况&#xff0c;可用于跨区域对比、时序趋势分析及经济关联性研究&#xff0c;既适合高校师生课题使用&#xff0c;也适合行业分析师快速取得基…

作者头像 李华