1. 项目概述从零构建一个“五脏俱全”的迷你文件系统最近在整理硬盘看着Windows资源管理器里层层叠叠的文件夹和文件突然想起大学时操作系统课设的那个“大作业”——用C模拟实现一个多用户多级目录的文件系统。这玩意儿听起来挺唬人好像要造一个Windows或Linux出来但实际上它的核心是让你理解文件系统这个“黑盒子”里到底是怎么运作的。它不是让你真的去读写物理磁盘扇区而是用内存模拟磁盘用数据结构模拟文件控制块FCB、目录项和用户权限表。说白了就是用代码搭建一个文件系统的“骨架”和“神经系统”让你能通过命令行去创建用户、登录、建目录、存文件体验一遍数据从逻辑名到物理存储的完整生命周期。这个项目特别适合有一定C基础想深入理解操作系统底层原理或者对系统编程感兴趣的朋友。它不像做网站或APP那样有直观的界面但每一步操作背后都对应着深刻的设计思想比如一个文件在磁盘上怎么表示多级目录的树形结构如何在内存中高效组织不同的用户凭什么只能访问自己的文件把这些想明白了你再去看Linux的ls -l命令输出、chmod权限设置甚至数据库的索引结构都会有豁然开朗的感觉。接下来我就把自己当年实现这个系统时踩过的坑、总结的经验以及一份可以直接“抄作业”的详细设计思路和核心代码拆解分享给大家。2. 核心设计思路与架构拆解2.1 为什么选择内存模拟而非真实磁盘很多新手一听到“文件系统”第一反应就是去操作硬盘的扇区。这其实是个误区也大大增加了项目的难度和风险。我们的目标是理解原理而非制造一个工业级产品。因此最经典也最安全的方法是内存模拟。具体来说我们会在程序启动时在内存中申请一大块连续的空间比如一个char数组把它虚拟成我们的“磁盘”。例如定义一个char my_disk[DISK_SIZE]假设DISK_SIZE是1024*10241MB。这1MB的空间就是我们的整个“硬盘”。接下来我们需要在这块“硬盘”上划分区域引导块可能不需要、超级块、索引节点区、数据区。这种设计模拟了Unix/Linux文件系统如ext2的经典布局。注意选择内存模拟的核心优势在于可重复性和安全性。程序崩溃不会损坏你的真实硬盘你可以随时重置“磁盘”状态方便调试。这也是几乎所有操作系统课程设计采用的标准方法。2.2 多用户与权限模型的设计“多用户”意味着系统需要识别不同的使用者并控制他们对文件的访问。一个简单而有效的模型是模仿Linux的“用户-组-其他”三级权限但为了简化我们可以先实现基于用户的直接权限控制。核心数据结构设计用户表一个全局数组或向量记录所有注册用户。struct User { std::string username; std::string password; // 实际应用中应存储哈希值课设为简化可存明文 int uid; // 用户唯一ID }; std::vectorUser user_table;文件控制块FCB或索引节点inode这是文件的“身份证”存储文件的元数据。关键点在于需要增加一个字段来标识文件的所有者。struct Inode { int i_uid; // 文件所有者的用户ID int i_mode; // 文件类型和权限例如0表示目录1表示普通文件权限位可以用整数表示如755 int i_size; // 文件大小字节 int i_block_count; // 占用的磁盘块数 int i_direct_blocks[10]; // 直接数据块指针假设每个数据块512字节 int i_indirect_block; // 一级间接块指针用于扩展 // ... 其他信息如创建时间 };权限检查逻辑任何文件操作读、写、删除前都必须进行权限检查。流程如下获取当前登录用户的UID。找到目标文件的Inode。比较当前用户UID与文件Inode中的i_uid。如果相同则为文件所有者拥有全部权限根据i_mode中的所有者权限位。如果不同则检查是否有“其他用户”权限。我们这里做了简化未引入“组”的概念。2.3 多级目录的树形结构实现这是项目的另一个核心难点。如何用代码表示/home/user1/docs/report.txt这样的路径方案选择目录项dentry与Inode分离这是现代文件系统的常见设计。Inode存储文件的元数据而目录本身是一个特殊的文件它的数据块里不存子文件的内容存的是一条条的目录项。每个目录项建立了“文件名”到“Inode编号”的映射。struct DirEntry { char filename[FILENAME_MAX_LEN]; // 文件名 int inode_no; // 该文件对应的Inode编号 };目录文件的存储一个目录的Inode其i_mode标记为“目录类型”。它指向的数据块里存放的就是若干个DirEntry结构体。例如home目录的数据块里可能有两个条目(user1, inode_no_100)和(user2, inode_no_101)。路径解析算法 当用户输入命令cd /home/user1/docs时从根目录开始根目录的Inode编号通常是固定的比如0。读取根目录的数据块查找名为home的目录项得到其Inode编号假设是10。加载Inode 10读取其数据块即home目录的内容查找名为user1的目录项得到其Inode编号假设是100。加载Inode 100读取其数据块查找名为docs的目录项得到其Inode编号假设是150。最终将当前工作目录切换到Inode 150所代表的目录。这个过程就是路径遍历是文件系统最频繁的操作之一。实现时需要编写一个path_resolve(const std::string path)函数它能处理绝对路径以/开头和相对路径不以/开头并返回目标文件或目录的Inode编号。3. 核心模块的详细实现与代码解析3.1 “磁盘”与存储管理器的实现我们首先需要封装这个虚拟磁盘提供一个类似磁盘驱动器的接口。class DiskSimulator { private: char* disk_space; // 指向模拟磁盘内存的指针 int disk_size; // 磁盘总大小 int block_size; // 磁盘块大小通常设为512B或1KB int total_blocks; // 总块数 std::bitsetMAX_BLOCKS block_bitmap; // 位示图用于管理空闲块 public: DiskSimulator(int size, int b_size); ~DiskSimulator(); // 读写磁盘块以块为单位进行IO bool read_block(int block_no, char* buffer); bool write_block(int block_no, const char* data); // 分配一个空闲块 int allocate_block(); // 释放一个块 bool free_block(int block_no); // 格式化磁盘初始化超级块、位图等 void format_disk(); };关键点block_bitmap位示图是管理空闲空间的核心数据结构。allocate_block()需要扫描位示图找到第一个为0空闲的位将其置1并返回块号。free_block()则将对应位置0。read/write_block是底层接口所有对文件的读写最终都要拆分成对若干个磁盘块的读写操作。format_disk()函数会在“磁盘”初始化时在固定位置如第0块写入超级块。超级块保存了文件系统的全局信息如Inode总数、数据块总数、空闲块数量、魔数用于标识文件系统类型等。系统启动时首先要读取超级块。3.2 文件与目录操作的核心API基于上面的存储管理器我们可以实现上层的文件操作。这里以创建文件和目录为例。class FileSystem { private: DiskSimulator disk; Inode* inode_table; // Inode表在内存中的缓存 User current_user; // 当前登录用户 Inode* current_dir_inode; // 当前工作目录的Inode public: // 创建文件 int create_file(const std::string path, int mode) { // 1. 路径解析确保父目录存在且用户有写权限 std::string parent_dir, filename; split_path(path, parent_dir, filename); Inode* parent_inode path_resolve(parent_dir); if (!parent_inode || !has_permission(parent_inode, WRITE_PERM)) { return -1; // 权限不足或路径错误 } // 2. 检查同名文件是否已存在 if (find_entry_in_dir(parent_inode, filename) ! -1) { return -2; // 文件已存在 } // 3. 分配一个新的Inode int new_inode_no allocate_inode(); Inode* new_inode inode_table[new_inode_no]; new_inode-i_uid current_user.uid; new_inode-i_mode mode | REGULAR_FILE; // 设置文件类型和权限 new_inode-i_size 0; // ... 初始化其他字段 // 4. 在父目录的数据块中增加一条目录项 DirEntry new_entry; strcpy(new_entry.filename, filename.c_str()); new_entry.inode_no new_inode_no; if (!add_entry_to_dir(parent_inode, new_entry)) { free_inode(new_inode_no); // 回滚 return -3; // 目录空间不足 } // 5. 同步更新父目录Inode和新的文件Inode到“磁盘” sync_inode_to_disk(parent_inode); sync_inode_to_disk(new_inode); return 0; // 成功 } // 创建目录与创建文件类似但Inode类型不同 int create_dir(const std::string path) { // ... 类似create_file // 不同点 // 1. 分配的Inode其 i_mode 设置为 DIR_TYPE // 2. 创建成功后需要立即初始化这个新目录的数据块至少包含两个默认目录项 // - (. 当前目录的inode_no) // - (.. 父目录的inode_no) // 这是Unix目录的标准结构cd . 和 cd .. 就依赖于此。 } // 删除文件unlink int delete_file(const std::string path) { // 1. 路径解析获取文件Inode和父目录Inode // 2. 权限检查用户必须是文件所有者或拥有父目录写权限 // 3. 将文件Inode的链接数减1我们简化实现可以认为链接数就是1 // 4. 如果链接数减为0 // a. 释放该文件占用的所有数据块根据i_direct_blocks等指针找到所有块调用disk.free_block // b. 释放该文件的Inode标记为空闲 // 5. 在父目录中删除对应的目录项 // 6. 同步父目录Inode到磁盘 } };实操心得路径解析是万恶之源一定要把path_resolve函数写健壮处理好/,./,../以及路径中可能存在的多余斜杠。建议单独为这个函数编写大量测试用例。权限检查要前置在任何实质性操作读、写、创建、删除之前必须完成路径解析和权限验证这是一个良好的安全编程习惯。事务性思维像create_file这种操作涉及多个步骤分配Inode、分配数据块、写目录项。要考虑到中间任何一步失败都需要能够回滚之前分配的资源避免留下“垃圾”数据占用空间。3.3 用户登录与会话管理文件系统的多用户特性需要一个登录和会话管理模块。class SessionManager { private: std::vectorUser users; User* logged_in_user; FileSystem* fs; // 关联的文件系统实例 public: bool login(const std::string username, const std::string password) { for (auto user : users) { if (user.username username user.password password) { // 实际应对比密码哈希 logged_in_user user; // 登录成功后将文件系统的当前用户设置为该用户 fs-set_current_user(user); // 通常用户登录后其当前目录会设置为家目录如 /home/username std::string home_path /home/ username; fs-change_directory(home_path); return true; } } return false; } void logout() { logged_in_user nullptr; fs-set_current_user(User{, , -1}); // 设置为无效用户 } bool register_user(const std::string username, const std::string password) { // 检查用户名是否已存在 // 分配新的UID // 创建用户家目录 /home/username // 将新用户加入users表 } };注意事项密码存储在真正的系统中绝对不能在数据库或文件里明文存储密码。必须使用加盐哈希如bcrypt, scrypt。课程设计中为了简化可以明文存储但必须在实验报告里明确指出这是不安全的并说明工业界的正确做法。家目录创建用户注册时自动为其在/home下创建一个以用户名命名的目录并将该目录的所有者设置为新用户。这模拟了Linux系统的行为。4. 实验报告的核心要点与撰写技巧实验报告不仅是代码的附属品更是展示你思考过程和理解深度的关键。一份优秀的实验报告应该包含以下部分4.1 需求分析与总体设计这部分要讲清楚“你要做什么”和“你打算怎么做”。功能需求列表用条目清晰列出系统必须实现的功能如用户登录/注册、创建/删除文件/目录、读写文件、切换目录、显示目录内容、修改文件权限等。非功能需求例如数据持久化模拟磁盘内容可保存到真实文件、友好的命令行交互等。总体架构图画一个简单的模块图说明DiskSimulator,FileSystem,SessionManager等核心类之间的关系和数据流。关键数据结构设计用表格或类图展示Inode,DirEntry,User,SuperBlock等结构体的详细字段和含义。4.2 详细设计与模块实现这是报告的技术核心。磁盘模拟模块解释位示图管理空闲块的原理和算法如首次适应。文件与目录管理重点阐述单级目录与多级目录在实现上的本质区别核心在于目录项和路径解析。详细说明文件的物理存储结构连续分配、链式分配、索引分配。你采用的是哪种为什么通常选择类Unix的索引分配即Inode带有直接、间接指针。用图示展示一个文件的数据块是如何通过Inode被找到的。目录文件的存储格式画图说明一个目录数据块里DirEntry是如何排列的。多用户与安全说明你的权限模型例如简化的UGO模型。给出一个权限检查的流程图或伪代码清晰展示“用户请求操作 - 检查文件所有者 - 检查权限位 - 允许/拒绝”的完整逻辑。关键算法路径解析算法的详细步骤和伪代码。空间分配算法如位示图查找空闲块。4.3 系统测试与结果分析不要只说“测试通过”要展示测试过程和证据。测试用例设计设计覆盖所有主要功能和边界条件的测试用例。正常流用户登录、创建目录、创建文件、写入内容、读出内容、注销。异常流无权限访问他人文件、创建同名文件、删除非空目录、磁盘空间不足、路径不存在等。测试结果展示粘贴命令行运行的真实截图并配文说明每一步在测试什么。截图是最有力的证据。核心数据结构快照在关键操作如创建大文件后后可以编写调试函数打印出内存中位示图的状态、Inode表的内容并解释其含义。这能极大体现你对底层原理的掌握。4.4 遇到的问题与解决方案这是报告中最能体现个人价值的部分。列举2-3个你遇到的最棘手的Bug或设计难题并说明你是如何分析和解决的。示例1删除文件后空间没有回收现象删除文件后创建新文件时依然提示磁盘满。排查检查删除文件的delete_file函数发现只删除了目录项但没有调用disk.free_block释放文件占用的数据块也没有标记Inode为空闲。解决在删除逻辑中添加遍历文件数据块并释放的代码以及回收Inode的代码。这引出了对“文件删除”本质的讨论删除目录项只是断开了链接当链接数为0时存储空间才能真正被回收。示例2路径../../解析错误导致系统崩溃现象在深层目录使用cd ../../有时会跳到奇怪的位置或程序崩溃。排查path_resolve函数在处理..时没有正确判断当前目录是否是根目录。在根目录下cd ..应该仍然停留在根目录。解决在解析到..时增加对当前Inode编号的判断。如果是根目录的Inode编号通常为0或固定值则忽略此次..。这加强了对文件系统树形结构根节点特殊性的理解。4.5 实验总结与心得体会不要写空话套话结合具体经历写。对文件系统组成的理解通过实现你明白了超级块、Inode、数据块、目录项各自的作用和联系。对“一切皆文件”的体会目录也是一种文件只是内容格式特殊。设备、管道在Unix中也是文件。你的设计是否体现了这种统一性对权限系统的认识实现一个简单的权限检查都如此繁琐可以想象真实操作系统安全模块的复杂性。工程能力的提升如何设计模块接口以保证低耦合如何编写可测试的代码如何用版本管理如Git来管理这个小型项目不足之处与展望坦诚说明你的简化版和真实文件系统如ext4, NTFS的差距在哪里例如没有实现软硬链接、没有日志功能、权限模型过于简单、没有考虑并发访问等。5. 项目扩展与进阶思考方向如果你已经完成了基础版本想让项目更出彩可以考虑以下扩展方向这些也常常是面试中深入讨论的话题1. 实现文件的打开与读写操作设计一个打开文件表。用户程序使用open()返回一个文件描述符fd内核中维护一个全局打开文件表记录打开模式、读写偏移量等信息。read()和write()操作通过fd找到打开文件表项再定位到Inode和数据块。这能让你理解用户态文件操作API和内核态实现之间的桥梁。2. 实现简单的文件描述符与文件句柄模仿Linux为每个进程维护一个文件描述符数组。open调用返回一个最小的未使用的fd。实现close,lseek等操作。这涉及到资源管理和进程隔离的概念。3. 引入“组”的概念完善权限模型在User结构体中增加gid组ID。在Inode中增加i_gid并完整实现9位权限码所有者、组、其他用户各3位rwx。修改权限检查逻辑先判断是否为所有者再判断是否为同组用户最后才是其他用户。4. 实现数据持久化目前“磁盘”在内存中程序退出就丢失。可以增加dump_to_file()和load_from_file()函数将整个my_disk数组以及内存中的一些管理数据结构如位示图、用户表序列化保存到本地一个真实的文件中。下次程序启动时从这个文件加载实现状态的持久化。这让你接触了对象序列化和持久化存储的概念。5. 实现简单的命令行解释器编写一个循环解析用户输入的命令如ls,mkdir,echo “content” file.txt,cat file.txt等。这需要设计一个简单的词法分析器能分割命令和参数并调用底层的文件系统API。这综合考察了字符串处理和模块设计能力。实现这个项目的过程就像亲手搭建了一个微型的数字世界。最初你可能被各种数据结构绕晕但当你第一次成功用mkdir和touch创建出层级结构第一次用自己写的cat读出文件内容时那种对复杂系统豁然开朗的成就感是单纯看书无法比拟的。它带给你的不仅是C编程和数据结构能力的提升更是一种系统性的、自底向上的思维方式。当你以后再使用任何操作系统时你看到的将不再是一个黑盒而是一个由超级块、Inode、数据块和目录项精密组合而成的、清晰可见的运转模型。
C++实现多用户文件系统:从Inode到权限管理的完整设计
发布时间:2026/7/16 15:05:37
1. 项目概述从零构建一个“五脏俱全”的迷你文件系统最近在整理硬盘看着Windows资源管理器里层层叠叠的文件夹和文件突然想起大学时操作系统课设的那个“大作业”——用C模拟实现一个多用户多级目录的文件系统。这玩意儿听起来挺唬人好像要造一个Windows或Linux出来但实际上它的核心是让你理解文件系统这个“黑盒子”里到底是怎么运作的。它不是让你真的去读写物理磁盘扇区而是用内存模拟磁盘用数据结构模拟文件控制块FCB、目录项和用户权限表。说白了就是用代码搭建一个文件系统的“骨架”和“神经系统”让你能通过命令行去创建用户、登录、建目录、存文件体验一遍数据从逻辑名到物理存储的完整生命周期。这个项目特别适合有一定C基础想深入理解操作系统底层原理或者对系统编程感兴趣的朋友。它不像做网站或APP那样有直观的界面但每一步操作背后都对应着深刻的设计思想比如一个文件在磁盘上怎么表示多级目录的树形结构如何在内存中高效组织不同的用户凭什么只能访问自己的文件把这些想明白了你再去看Linux的ls -l命令输出、chmod权限设置甚至数据库的索引结构都会有豁然开朗的感觉。接下来我就把自己当年实现这个系统时踩过的坑、总结的经验以及一份可以直接“抄作业”的详细设计思路和核心代码拆解分享给大家。2. 核心设计思路与架构拆解2.1 为什么选择内存模拟而非真实磁盘很多新手一听到“文件系统”第一反应就是去操作硬盘的扇区。这其实是个误区也大大增加了项目的难度和风险。我们的目标是理解原理而非制造一个工业级产品。因此最经典也最安全的方法是内存模拟。具体来说我们会在程序启动时在内存中申请一大块连续的空间比如一个char数组把它虚拟成我们的“磁盘”。例如定义一个char my_disk[DISK_SIZE]假设DISK_SIZE是1024*10241MB。这1MB的空间就是我们的整个“硬盘”。接下来我们需要在这块“硬盘”上划分区域引导块可能不需要、超级块、索引节点区、数据区。这种设计模拟了Unix/Linux文件系统如ext2的经典布局。注意选择内存模拟的核心优势在于可重复性和安全性。程序崩溃不会损坏你的真实硬盘你可以随时重置“磁盘”状态方便调试。这也是几乎所有操作系统课程设计采用的标准方法。2.2 多用户与权限模型的设计“多用户”意味着系统需要识别不同的使用者并控制他们对文件的访问。一个简单而有效的模型是模仿Linux的“用户-组-其他”三级权限但为了简化我们可以先实现基于用户的直接权限控制。核心数据结构设计用户表一个全局数组或向量记录所有注册用户。struct User { std::string username; std::string password; // 实际应用中应存储哈希值课设为简化可存明文 int uid; // 用户唯一ID }; std::vectorUser user_table;文件控制块FCB或索引节点inode这是文件的“身份证”存储文件的元数据。关键点在于需要增加一个字段来标识文件的所有者。struct Inode { int i_uid; // 文件所有者的用户ID int i_mode; // 文件类型和权限例如0表示目录1表示普通文件权限位可以用整数表示如755 int i_size; // 文件大小字节 int i_block_count; // 占用的磁盘块数 int i_direct_blocks[10]; // 直接数据块指针假设每个数据块512字节 int i_indirect_block; // 一级间接块指针用于扩展 // ... 其他信息如创建时间 };权限检查逻辑任何文件操作读、写、删除前都必须进行权限检查。流程如下获取当前登录用户的UID。找到目标文件的Inode。比较当前用户UID与文件Inode中的i_uid。如果相同则为文件所有者拥有全部权限根据i_mode中的所有者权限位。如果不同则检查是否有“其他用户”权限。我们这里做了简化未引入“组”的概念。2.3 多级目录的树形结构实现这是项目的另一个核心难点。如何用代码表示/home/user1/docs/report.txt这样的路径方案选择目录项dentry与Inode分离这是现代文件系统的常见设计。Inode存储文件的元数据而目录本身是一个特殊的文件它的数据块里不存子文件的内容存的是一条条的目录项。每个目录项建立了“文件名”到“Inode编号”的映射。struct DirEntry { char filename[FILENAME_MAX_LEN]; // 文件名 int inode_no; // 该文件对应的Inode编号 };目录文件的存储一个目录的Inode其i_mode标记为“目录类型”。它指向的数据块里存放的就是若干个DirEntry结构体。例如home目录的数据块里可能有两个条目(user1, inode_no_100)和(user2, inode_no_101)。路径解析算法 当用户输入命令cd /home/user1/docs时从根目录开始根目录的Inode编号通常是固定的比如0。读取根目录的数据块查找名为home的目录项得到其Inode编号假设是10。加载Inode 10读取其数据块即home目录的内容查找名为user1的目录项得到其Inode编号假设是100。加载Inode 100读取其数据块查找名为docs的目录项得到其Inode编号假设是150。最终将当前工作目录切换到Inode 150所代表的目录。这个过程就是路径遍历是文件系统最频繁的操作之一。实现时需要编写一个path_resolve(const std::string path)函数它能处理绝对路径以/开头和相对路径不以/开头并返回目标文件或目录的Inode编号。3. 核心模块的详细实现与代码解析3.1 “磁盘”与存储管理器的实现我们首先需要封装这个虚拟磁盘提供一个类似磁盘驱动器的接口。class DiskSimulator { private: char* disk_space; // 指向模拟磁盘内存的指针 int disk_size; // 磁盘总大小 int block_size; // 磁盘块大小通常设为512B或1KB int total_blocks; // 总块数 std::bitsetMAX_BLOCKS block_bitmap; // 位示图用于管理空闲块 public: DiskSimulator(int size, int b_size); ~DiskSimulator(); // 读写磁盘块以块为单位进行IO bool read_block(int block_no, char* buffer); bool write_block(int block_no, const char* data); // 分配一个空闲块 int allocate_block(); // 释放一个块 bool free_block(int block_no); // 格式化磁盘初始化超级块、位图等 void format_disk(); };关键点block_bitmap位示图是管理空闲空间的核心数据结构。allocate_block()需要扫描位示图找到第一个为0空闲的位将其置1并返回块号。free_block()则将对应位置0。read/write_block是底层接口所有对文件的读写最终都要拆分成对若干个磁盘块的读写操作。format_disk()函数会在“磁盘”初始化时在固定位置如第0块写入超级块。超级块保存了文件系统的全局信息如Inode总数、数据块总数、空闲块数量、魔数用于标识文件系统类型等。系统启动时首先要读取超级块。3.2 文件与目录操作的核心API基于上面的存储管理器我们可以实现上层的文件操作。这里以创建文件和目录为例。class FileSystem { private: DiskSimulator disk; Inode* inode_table; // Inode表在内存中的缓存 User current_user; // 当前登录用户 Inode* current_dir_inode; // 当前工作目录的Inode public: // 创建文件 int create_file(const std::string path, int mode) { // 1. 路径解析确保父目录存在且用户有写权限 std::string parent_dir, filename; split_path(path, parent_dir, filename); Inode* parent_inode path_resolve(parent_dir); if (!parent_inode || !has_permission(parent_inode, WRITE_PERM)) { return -1; // 权限不足或路径错误 } // 2. 检查同名文件是否已存在 if (find_entry_in_dir(parent_inode, filename) ! -1) { return -2; // 文件已存在 } // 3. 分配一个新的Inode int new_inode_no allocate_inode(); Inode* new_inode inode_table[new_inode_no]; new_inode-i_uid current_user.uid; new_inode-i_mode mode | REGULAR_FILE; // 设置文件类型和权限 new_inode-i_size 0; // ... 初始化其他字段 // 4. 在父目录的数据块中增加一条目录项 DirEntry new_entry; strcpy(new_entry.filename, filename.c_str()); new_entry.inode_no new_inode_no; if (!add_entry_to_dir(parent_inode, new_entry)) { free_inode(new_inode_no); // 回滚 return -3; // 目录空间不足 } // 5. 同步更新父目录Inode和新的文件Inode到“磁盘” sync_inode_to_disk(parent_inode); sync_inode_to_disk(new_inode); return 0; // 成功 } // 创建目录与创建文件类似但Inode类型不同 int create_dir(const std::string path) { // ... 类似create_file // 不同点 // 1. 分配的Inode其 i_mode 设置为 DIR_TYPE // 2. 创建成功后需要立即初始化这个新目录的数据块至少包含两个默认目录项 // - (. 当前目录的inode_no) // - (.. 父目录的inode_no) // 这是Unix目录的标准结构cd . 和 cd .. 就依赖于此。 } // 删除文件unlink int delete_file(const std::string path) { // 1. 路径解析获取文件Inode和父目录Inode // 2. 权限检查用户必须是文件所有者或拥有父目录写权限 // 3. 将文件Inode的链接数减1我们简化实现可以认为链接数就是1 // 4. 如果链接数减为0 // a. 释放该文件占用的所有数据块根据i_direct_blocks等指针找到所有块调用disk.free_block // b. 释放该文件的Inode标记为空闲 // 5. 在父目录中删除对应的目录项 // 6. 同步父目录Inode到磁盘 } };实操心得路径解析是万恶之源一定要把path_resolve函数写健壮处理好/,./,../以及路径中可能存在的多余斜杠。建议单独为这个函数编写大量测试用例。权限检查要前置在任何实质性操作读、写、创建、删除之前必须完成路径解析和权限验证这是一个良好的安全编程习惯。事务性思维像create_file这种操作涉及多个步骤分配Inode、分配数据块、写目录项。要考虑到中间任何一步失败都需要能够回滚之前分配的资源避免留下“垃圾”数据占用空间。3.3 用户登录与会话管理文件系统的多用户特性需要一个登录和会话管理模块。class SessionManager { private: std::vectorUser users; User* logged_in_user; FileSystem* fs; // 关联的文件系统实例 public: bool login(const std::string username, const std::string password) { for (auto user : users) { if (user.username username user.password password) { // 实际应对比密码哈希 logged_in_user user; // 登录成功后将文件系统的当前用户设置为该用户 fs-set_current_user(user); // 通常用户登录后其当前目录会设置为家目录如 /home/username std::string home_path /home/ username; fs-change_directory(home_path); return true; } } return false; } void logout() { logged_in_user nullptr; fs-set_current_user(User{, , -1}); // 设置为无效用户 } bool register_user(const std::string username, const std::string password) { // 检查用户名是否已存在 // 分配新的UID // 创建用户家目录 /home/username // 将新用户加入users表 } };注意事项密码存储在真正的系统中绝对不能在数据库或文件里明文存储密码。必须使用加盐哈希如bcrypt, scrypt。课程设计中为了简化可以明文存储但必须在实验报告里明确指出这是不安全的并说明工业界的正确做法。家目录创建用户注册时自动为其在/home下创建一个以用户名命名的目录并将该目录的所有者设置为新用户。这模拟了Linux系统的行为。4. 实验报告的核心要点与撰写技巧实验报告不仅是代码的附属品更是展示你思考过程和理解深度的关键。一份优秀的实验报告应该包含以下部分4.1 需求分析与总体设计这部分要讲清楚“你要做什么”和“你打算怎么做”。功能需求列表用条目清晰列出系统必须实现的功能如用户登录/注册、创建/删除文件/目录、读写文件、切换目录、显示目录内容、修改文件权限等。非功能需求例如数据持久化模拟磁盘内容可保存到真实文件、友好的命令行交互等。总体架构图画一个简单的模块图说明DiskSimulator,FileSystem,SessionManager等核心类之间的关系和数据流。关键数据结构设计用表格或类图展示Inode,DirEntry,User,SuperBlock等结构体的详细字段和含义。4.2 详细设计与模块实现这是报告的技术核心。磁盘模拟模块解释位示图管理空闲块的原理和算法如首次适应。文件与目录管理重点阐述单级目录与多级目录在实现上的本质区别核心在于目录项和路径解析。详细说明文件的物理存储结构连续分配、链式分配、索引分配。你采用的是哪种为什么通常选择类Unix的索引分配即Inode带有直接、间接指针。用图示展示一个文件的数据块是如何通过Inode被找到的。目录文件的存储格式画图说明一个目录数据块里DirEntry是如何排列的。多用户与安全说明你的权限模型例如简化的UGO模型。给出一个权限检查的流程图或伪代码清晰展示“用户请求操作 - 检查文件所有者 - 检查权限位 - 允许/拒绝”的完整逻辑。关键算法路径解析算法的详细步骤和伪代码。空间分配算法如位示图查找空闲块。4.3 系统测试与结果分析不要只说“测试通过”要展示测试过程和证据。测试用例设计设计覆盖所有主要功能和边界条件的测试用例。正常流用户登录、创建目录、创建文件、写入内容、读出内容、注销。异常流无权限访问他人文件、创建同名文件、删除非空目录、磁盘空间不足、路径不存在等。测试结果展示粘贴命令行运行的真实截图并配文说明每一步在测试什么。截图是最有力的证据。核心数据结构快照在关键操作如创建大文件后后可以编写调试函数打印出内存中位示图的状态、Inode表的内容并解释其含义。这能极大体现你对底层原理的掌握。4.4 遇到的问题与解决方案这是报告中最能体现个人价值的部分。列举2-3个你遇到的最棘手的Bug或设计难题并说明你是如何分析和解决的。示例1删除文件后空间没有回收现象删除文件后创建新文件时依然提示磁盘满。排查检查删除文件的delete_file函数发现只删除了目录项但没有调用disk.free_block释放文件占用的数据块也没有标记Inode为空闲。解决在删除逻辑中添加遍历文件数据块并释放的代码以及回收Inode的代码。这引出了对“文件删除”本质的讨论删除目录项只是断开了链接当链接数为0时存储空间才能真正被回收。示例2路径../../解析错误导致系统崩溃现象在深层目录使用cd ../../有时会跳到奇怪的位置或程序崩溃。排查path_resolve函数在处理..时没有正确判断当前目录是否是根目录。在根目录下cd ..应该仍然停留在根目录。解决在解析到..时增加对当前Inode编号的判断。如果是根目录的Inode编号通常为0或固定值则忽略此次..。这加强了对文件系统树形结构根节点特殊性的理解。4.5 实验总结与心得体会不要写空话套话结合具体经历写。对文件系统组成的理解通过实现你明白了超级块、Inode、数据块、目录项各自的作用和联系。对“一切皆文件”的体会目录也是一种文件只是内容格式特殊。设备、管道在Unix中也是文件。你的设计是否体现了这种统一性对权限系统的认识实现一个简单的权限检查都如此繁琐可以想象真实操作系统安全模块的复杂性。工程能力的提升如何设计模块接口以保证低耦合如何编写可测试的代码如何用版本管理如Git来管理这个小型项目不足之处与展望坦诚说明你的简化版和真实文件系统如ext4, NTFS的差距在哪里例如没有实现软硬链接、没有日志功能、权限模型过于简单、没有考虑并发访问等。5. 项目扩展与进阶思考方向如果你已经完成了基础版本想让项目更出彩可以考虑以下扩展方向这些也常常是面试中深入讨论的话题1. 实现文件的打开与读写操作设计一个打开文件表。用户程序使用open()返回一个文件描述符fd内核中维护一个全局打开文件表记录打开模式、读写偏移量等信息。read()和write()操作通过fd找到打开文件表项再定位到Inode和数据块。这能让你理解用户态文件操作API和内核态实现之间的桥梁。2. 实现简单的文件描述符与文件句柄模仿Linux为每个进程维护一个文件描述符数组。open调用返回一个最小的未使用的fd。实现close,lseek等操作。这涉及到资源管理和进程隔离的概念。3. 引入“组”的概念完善权限模型在User结构体中增加gid组ID。在Inode中增加i_gid并完整实现9位权限码所有者、组、其他用户各3位rwx。修改权限检查逻辑先判断是否为所有者再判断是否为同组用户最后才是其他用户。4. 实现数据持久化目前“磁盘”在内存中程序退出就丢失。可以增加dump_to_file()和load_from_file()函数将整个my_disk数组以及内存中的一些管理数据结构如位示图、用户表序列化保存到本地一个真实的文件中。下次程序启动时从这个文件加载实现状态的持久化。这让你接触了对象序列化和持久化存储的概念。5. 实现简单的命令行解释器编写一个循环解析用户输入的命令如ls,mkdir,echo “content” file.txt,cat file.txt等。这需要设计一个简单的词法分析器能分割命令和参数并调用底层的文件系统API。这综合考察了字符串处理和模块设计能力。实现这个项目的过程就像亲手搭建了一个微型的数字世界。最初你可能被各种数据结构绕晕但当你第一次成功用mkdir和touch创建出层级结构第一次用自己写的cat读出文件内容时那种对复杂系统豁然开朗的成就感是单纯看书无法比拟的。它带给你的不仅是C编程和数据结构能力的提升更是一种系统性的、自底向上的思维方式。当你以后再使用任何操作系统时你看到的将不再是一个黑盒而是一个由超级块、Inode、数据块和目录项精密组合而成的、清晰可见的运转模型。