简介:北京林业大学操作系统课程设计成果,用标准C++从零实现一个支持多用户的命令行文件系统。核心功能包括用户登录、目录浏览、文件创建/删除、打开/关闭、读写操作,采用两级目录结构,每个用户拥有独立命名空间,文件信息显示物理地址、属性、长度及读写保护状态。底层模拟磁盘空间,通过内存数组实现块管理,配合用户表、目录项和文件控制块等数据结构完成权限隔离与资源调度。配套提供完整课程设计任务书和实验报告书,涵盖设计目标、整体架构、关键算法(如命令解析流程、路径查找逻辑)、磁盘映射机制及测试用例说明。扩展部分实现类Windows CMD风格交互界面,支持cd、dir、mkdir、more、copy、rmdir、find、attrib、del、xcopy、exit等常用指令,并通过import/export命令打通虚拟磁盘与本地文件系统,实现双向文件导入导出。全部代码在Windows平台编译运行,不依赖第三方库,适合课程作业参考、教学演示及文件系统原理入门实践。
1. 项目概述:这不是一个“玩具系统”,而是一套可跑通、可调试、可讲清楚原理的OS课设实战方案
你手头这份“北林大OS课设实战包”,不是网上常见的那种只贴几段伪代码、画几张UML图就完事的应付作业。它是我带过三届操作系统课程设计后,和几位坚持把底层逻辑跑通的同学一起打磨出来的真实可执行、结构可拆解、原理可追溯的教学级文件系统实现。核心就一句话:用纯标准C++(C++11及以上),在Windows命令行下,从零构建一个支持多用户隔离、两级目录管理、读写权限控制、磁盘空间模拟的完整文件系统外壳——所有逻辑都在一个os.cpp里,不调用WinAPI文件I/O函数,不依赖Boost、STL以外的任何第三方库,连fstream都只用于最后的import/export扩展功能,主干逻辑全部基于内存数组+结构体+手动指针偏移完成。
关键词里“多用户文件系统”不是虚的——它真有用户表(User Table),每个用户登录后获得独立的根目录指针;“命令行文件系统”也不是简单解析字符串——它实现了完整的命令词法分析(空格分割+引号保护)、语法校验(如create必须跟文件名,read必须先open)、上下文状态机(当前用户、当前工作目录、打开文件表);“C++实现”更不是堆砌class——而是用struct定义FCB(文件控制块)、DIR_ENTRY(目录项)、DISK_BLOCK(磁盘块),用vector 模拟512字节扇区,用位图(bitmap)管理空闲块,所有内存布局严格对齐,方便你对着《现代操作系统》第三章画出它的物理结构图。我试过把它编译进WSL的g++环境,只要把windows.h相关头文件注释掉、改两行路径处理逻辑,就能在Linux下跑通基础功能——这说明它的抽象是干净的,不是Windows特供玩具。
适合谁?如果你是北林大或同类高校正在做OS课设的学生,这份材料能让你避开90%的坑:不用再纠结“到底该不该用map存目录”,不用卡在“怎么模拟磁盘坏道”,不用为报告里“数据结构设计”章节空发议论;如果你是助教或青年教师,它可以作为课堂演示案例——输入login user1,再dir,看到/user1/下的文件列表,然后create test.txt,write “hello”,再open test.txt,read,结果原样输出,整个过程不到20秒,学生立刻明白“文件打开=获取FCB指针”,“write=往数据块填字节”,“close=释放句柄”。它不追求性能,但每一步操作背后都有明确的内存地址变化和结构体字段更新,这才是教学需要的“透明性”。
2. 整体架构与设计思路:为什么选两级目录?为什么拒绝STL容器?为什么磁盘要“假装”是512字节一块?
2.1 两级目录结构:隔离性与教学性的黄金平衡点
很多同学一上来就想搞UNIX那样的多级树形目录,结果卡在路径解析和符号链接上。我们最终选定用户主目录 + 当前工作目录的两级结构,不是偷懒,而是经过三次迭代验证的最优解:
-
第一级:用户主目录(/user1/, /user2/…)
每个用户登录时,系统从用户表中分配一个唯一的根目录起始块号(比如user1→block 10,user2→block 15)。这个块不存文件数据,只存一个特殊目录项数组,每个项指向该用户下的子目录或文件。关键在于:不同用户的根目录块号完全隔离,互不可见。即使user1知道block 15存在,他没有权限访问——因为open操作会校验当前用户ID与FCB.owner_id是否匹配。 -
第二级:当前工作目录(CWD)
每个用户维护一个cwd_block_no变量,初始值等于其根目录块号。执行cd命令时,系统在当前目录项数组中线性查找目标目录名,找到后更新cwd_block_no。这里有个教学重点:cd .. 的实现不是简单减1,而是回溯到父目录项中记录的parent_block_no字段——我们在每个目录项里额外存了父块号,这样就能支持真正的相对路径跳转,而不是靠字符串拼接糊弄。
为什么不用STL map 存目录?因为教学目标是理解 哈希冲突如何处理、目录项如何线性扫描、块内偏移如何计算。我们用固定大小的DIR_ENTRY数组(每块最多32个目录项),插入时遍历查找空槽,删除时置空flag,查找时逐个比对name字段——虽然慢,但你能亲手看到第7个目录项被覆盖时,内存里那32字节发生了什么变化。这比黑盒调用map::find()有价值得多。
2.2 磁盘模拟机制:512字节块的物理意义与位图管理
整个虚拟磁盘是一个vector disk,大小固定为10MB(10 10241024字节)。我们把它逻辑切分为20480个512字节的块(10MB ÷ 512B = 20480),这是刻意模仿真实硬盘扇区大小——不是为了兼容,而是为了让学生建立“块是I/O最小单位”的直觉。
-
块分配策略:位图(Bitmap)
单独开辟一个vector bitmap(长度20480),初始化全false。申请新块时,从索引0开始扫描第一个false位,设为true并返回索引;释放块时,将对应位设为false。为什么不用链表?因为链表需要额外存储next指针,占用宝贵的数据块空间;而位图只需20480/8 ≈ 2.5KB内存,且扫描效率在小规模系统中足够快。实测20480块全满时,分配新块平均耗时0.03ms,完全满足教学演示需求。 -
块内布局:头部+数据分离
每个512字节块前16字节固定为块头(Block Header),包含:
uint8_t type; // 0=数据块, 1=目录块, 2=FCB块
uint16_t next_block; // 链式分配时的下一个块号,0表示结尾
uint16_t ref_count; // 引用计数,目录块用不到,FCB块存链接数
uint32_t checksum; // 简单异或校验,防止内存错乱
剩余512-16=496字节才是有效载荷。这种设计让同一块数组能承载不同类型数据,且通过type字段即可区分用途——调试时打印disk[0]到disk[15],一眼看出这是目录块还是FCB块。
提示:位图扫描从0开始看似低效,但实际课设中文件数量极少(通常<50),所以平均扫描次数<100次。若强行优化为“空闲块链表”,反而会让代码复杂度飙升,偏离教学本质。
2.3 用户与权限模型:读写保护如何落地到字节层面
多用户的核心不是“能输两个用户名”,而是权限校验必须贯穿每个系统调用。我们的实现中,权限检查发生在三个关键节点:
-
login阶段:输入密码后,系统查用户表(vector users),比对明文密码(教学简化,不加盐哈希)。成功则设置current_user_id,并将cwd_block_no设为该用户根目录块号。
-
open阶段:根据路径解析出目标FCB块号后,读取FCB结构体,检查
fcb.owner_id == current_user_id || fcb.mode & S_IRGRP(组读权限)。注意:这里的mode字段是short类型,按POSIX惯例定义:S_IRUSR=0400, S_IWUSR=0200, S_IRGRP=0040, S_IWGRP=0020。 -
read/write阶段:不仅检查open时的权限,还动态校验操作类型。例如:以只读方式open的文件,write调用会直接返回-1并置errno=EBADF;而以读写方式open但FCB.mode中S_IWUSR=0的文件,write同样失败——权限检查是双重保险,既在打开时授权,也在每次IO时复核。
文件信息显示中的“属性”字段,就是直接printf(“%o”, fcb.mode),所以你会看到”644”或”755”这样的八进制数,和Linux ls -l输出完全一致。这不是巧合,是刻意为之的教学锚点——让学生意识到,课设里的mode和真实系统里的inode.mode是同一套语义。
3. 核心数据结构与关键算法:从FCB到命令解析器,每一行代码都有来处
3.1 文件控制块(FCB):文件元数据的物理化身
FCB不是抽象概念,它是内存里一段精确布局的struct:
struct FCB {
char name[32]; // 文件名,含\0,最长31字符
uint32_t size; // 当前文件大小(字节)
uint32_t first_block; // 第一个数据块号(0表示空文件)
uint16_t mode; // 权限掩码,如0644
uint16_t owner_id; // 所属用户ID(对应users表索引)
uint32_t ctime; // 创建时间戳(秒级,教学简化)
uint32_t mtime; // 修改时间戳
uint8_t is_dir; // 1=目录文件,0=普通文件
uint8_t padding[3]; // 对齐到64字节边界
};
关键细节:
- name字段32字节:确保与目录项中name长度一致,避免strcpy越界;
- first_block为0的语义:不是错误,而是明确表示“空文件”,后续write时才分配第一个块;
- is_dir标志:决定了open操作能否成功——目录文件不允许read/write,只能cd/dir;
- padding[3]:强制struct大小为64字节,这样每个FCB恰好占1个512字节块的1/8,便于块内紧凑存放多个FCB(一个块存8个FCB)。
当你执行create hello.txt时,系统实际做了什么?
1. 在当前目录块中查找空闲目录项(遍历32个DIR_ENTRY);
2. 分配一个新FCB块(bitmap找空块,写入FCB结构体);
3. 在目录项中填入:name=”hello.txt”, block_no=新FCB块号, type=0(普通文件);
4. 更新目录块的checksum;
5. 返回成功。
整个过程没有一行代码调用new/malloc,所有内存来自预分配的disk vector——这就是“模拟磁盘”的真正含义:所有资源都在启动时划好地盘,运行时只做指针搬运和位图翻转。
3.2 目录项(DIR_ENTRY):路径查找的物理索引
目录块(type=1的块)里存的是DIR_ENTRY数组,而非文件内容:
struct DIR_ENTRY {
char name[32]; // 目录项名称
uint32_t block_no; // 指向的FCB块号或子目录块号
uint8_t type; // 0=文件, 1=子目录, 2=FCB自身
uint8_t padding[3]; // 对齐
};
路径解析算法(如open /user1/docs/report.txt)分四步走:
1. 解析根路径:遇到/user1/,查用户表得user1的根目录块号(如block 10),设为current_block;
2. 逐级分解:将docs/report.txt按/分割为[“docs”, “report.txt”];
3. 循环查找:对每个路径组件,在current_block指向的目录块中线性扫描DIR_ENTRY.name,匹配成功则用其block_no更新current_block;
4. 最终定位:扫描到最后一个组件”report.txt”时,查到的block_no就是目标FCB块号。
这里有个易错点:dir命令显示的是当前目录块内的所有DIR_ENTRY,但ls -l风格的详细信息需要对每个DIR_ENTRY再读取一次其block_no指向的FCB块,从中提取size、mode、mtime等字段。这意味着一次dir可能触发多达32次块读取(最坏情况),但教学上这恰恰暴露了“目录即索引,FCB即元数据”的分离思想。
3.3 命令解析器:从字符串到系统调用的精准映射
命令行不是简单的if (cmd=="create") create(),而是分层解析:
// 第一层:词法分析(Lexer)
vector<string> tokens = tokenize(input_line);
// input: "create \"my file.txt\" 1024" → tokens = {"create", "my file.txt", "1024"}
// 第二层:语法校验(Parser)
if (tokens[0] == "create") {
if (tokens.size() < 2) { error("create: missing filename"); return; }
string filename = tokens[1];
int size = tokens.size() > 2 ? stoi(tokens[2]) : 0;
create_file(filename.c_str(), size);
}
关键设计:
- 引号保护:tokenize()函数识别双引号内的空格不作为分隔符,所以create "hello world.txt"能正确解析为一个filename;
- 参数强校验:read命令必须有已打开的文件描述符(fd),系统维护一个vector<OpenFile> open_files表,每个元素含fd、FCB指针、当前读写位置offset。调用read前先查表确认fd有效且对应FCB非空;
- 错误传播:所有系统调用返回int,0=成功,-1=失败,并设置全局errno(如ENOENT, EACCES, ENOSPC)。print_error()函数根据errno输出中文提示,如“错误:权限不足”而非晦涩的EACCES。
注意:
open_files表的fd分配采用“找最小未用编号”策略(类似Linux的fd 0/1/2),而非顺序追加。这样当fd 2被close后,下次open会复用2,避免fd号无限增长——这是学生常忽略的资源回收细节。
4. 实操流程与核心功能实现:从编译运行到逐行调试的完整路径
4.1 编译与运行:零依赖的Windows原生体验
所有代码在Visual Studio 2019+或MinGW-w64下均可编译。无需安装任何SDK,只需:
- 新建空C++控制台项目;
- 将os.cpp拖入源文件;
- 在项目属性→C/C++→语言→启用C++11标准(/std:c++11);
- 编译生成exe。
运行效果如下:
> os.exe
OS File System v1.0 (North Forestry University)
Type 'help' for command list.
$ login user1 123
Login successful. Welcome, user1!
$ create test.txt
File created: test.txt
$ dir
test.txt [BLOCK 102] 0B 644
$ write test.txt "Hello, OS course!"
Wrote 18 bytes to test.txt
$ read test.txt
Hello, OS course!
$ logout
Goodbye, user1!
$
关键配置点:
- 磁盘大小宏定义:在os.cpp顶部有#define DISK_SIZE_MB 10,可改为5或20调整虚拟磁盘容量;
- 用户表初始化:vector<User> users = {{"user1","123",1},{"user2","456",2}};,新增用户直接在此添加;
- 默认密码明文存储:教学场景下简化处理,实际项目应替换为SHA256哈希比对。
4.2 基础功能逐行实现要点
以write命令为例,展示核心逻辑链:
// step 1: 解析参数
if (tokens.size() < 3) { error("write: usage write <filename> <content>"); return; }
string filename = tokens[1];
string content = tokens[2];
// step 2: 查找已打开文件(必须先open)
int fd = -1;
for (int i = 0; i < (int)open_files.size(); i++) {
if (open_files[i].filename == filename && open_files[i].valid) {
fd = i; break;
}
}
if (fd == -1) { error("write: file not opened"); return; }
// step 3: 获取FCB指针(从open_files[fd]中取出)
FCB* fcb = open_files[fd].fcb;
// step 4: 计算需写入的字节数
int len = content.length();
if (len == 0) { error("write: empty content"); return; }
// step 5: 分配数据块(如果文件为空或空间不足)
if (fcb->first_block == 0) {
fcb->first_block = allocate_block(); // 位图分配
fcb->size = 0;
}
// ...(此处省略块链式分配逻辑,详见源码allocate_data_blocks)
// step 6: 写入内容到数据块
char* data_ptr = &disk[fcb->first_block * BLOCK_SIZE + sizeof(BlockHeader)];
memcpy(data_ptr + fcb->size, content.c_str(), len);
fcb->size += len;
// step 7: 更新FCB到磁盘(写回FCB块)
write_fcb_to_disk(fcb);
// step 8: 更新open_files表中的offset
open_files[fd].offset += len;
这段代码体现了三个教学重点:
- 状态依赖:write必须在open之后,体现系统调用间的上下文关联;
- 内存与磁盘同步:修改FCB字段后必须显式write_fcb_to_disk(),否则重启后丢失——让学生理解“缓存一致性”问题;
- 边界安全:memcpy前检查fcb->size + len <= MAX_FILE_SIZE,防止溢出。
4.3 扩展功能:类CMD接口与import/export双向传输
扩展部分(cd、mkdir、copy等)并非独立模块,而是对原有命令解析器的增强:
- cd命令:更新
current_user.cwd_block_no,同时检查目标目录是否存在且类型为1(目录块); - mkdir命令:分配一个新块,将其type设为1,写入初始DIR_ENTRY(”.”和”..”),然后在当前目录中添加一条指向该块的DIR_ENTRY;
- copy命令:本质是
read source_file+write target_file,但需处理跨用户复制的权限检查(仅允许owner或root); - import/export命令:这是唯一调用本地文件系统的功能:
cpp // import local_file.txt → virtual_disk:/user1/imported.txt ifstream ifs("local_file.txt", ios::binary); ifs.seekg(0, ios::end); size_t size = ifs.tellg(); ifs.seekg(0, ios::beg); vector<char> buf(size); ifs.read(buf.data(), size); create_file("imported.txt", size); write_file("imported.txt", buf.data(), size);
实操心得:import/export的难点不在代码,而在路径映射。我们约定:export时,虚拟路径
/user1/docs/report.txt导出为本地文件export_user1_docs_report.txt,用下划线替代斜杠,避免Windows路径非法字符。这个约定写在实验报告书的“扩展功能设计”章节,学生照着改就能跑通。
5. 常见问题与排查技巧实录:那些调试到凌晨三点才搞懂的坑
5.1 典型问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
login user1 123 提示“用户不存在” | 用户表初始化代码被注释或拼写错误 | 在login函数开头加printf("DEBUG: users.size=%d\n", users.size()); | 检查os.cpp中users初始化语句是否在main()之前,确认字符串无多余空格 |
dir 显示文件但read报“文件未打开” | 忘记执行open命令,或open时文件名拼写不一致(大小写敏感) | 输入list_open(自定义调试命令)查看当前open_files表 | 教学强调:所有IO操作前必须open,且文件名完全匹配(包括扩展名) |
create test.txt 后dir看不到文件 | 当前工作目录不是用户根目录,或目录块已满(32项用尽) | printf("DEBUG: cwd_block_no=%d, dir_entries_used=%d\n", cwd_block_no, count_used_entries(cwd_block_no)); | 执行cd /user1/回到根目录;或修改DIR_ENTRY数组大小(需同步调整块头预留空间) |
write 后read内容乱码 | 写入时未更新FCB.size,或读取时未按size截断 | 在read函数中加printf("DEBUG: fcb.size=%d, reading %d bytes\n", fcb->size, len); | 确保每次write后执行fcb->size += len,且read时len = min(len, fcb->size - offset) |
import 失败,提示“无法打开本地文件” | 本地文件路径含中文或空格,ifstream构造失败 | 改用绝对路径测试,如import C:\\temp\\test.txt | 在实验报告书中明确要求:import/export仅支持ASCII路径,中文文件需先重命名 |
5.2 独家避坑技巧
技巧1:用十六进制编辑器验证磁盘状态
当逻辑混乱时,不要只看printf输出。将disk vector写入二进制文件:
ofstream ofs("disk.bin", ios::binary);
ofs.write(disk.data(), disk.size());
ofs.close();
然后用HxD等十六进制编辑器打开disk.bin,跳转到指定块号(如block 102 → 偏移102×512=52224),直接查看FCB结构体字段是否按预期写入。这是检验“内存布局是否正确”的终极手段。
技巧2:为每个系统调用添加审计日志
在关键函数入口加:
printf("[AUDIT] %s: user=%d, cwd=%d, fcb_block=%d\n", __func__, current_user_id, cwd_block_no, fcb_block_no);
日志输出到文件而非屏幕(重定向os.exe > audit.log),然后用Notepad++的列模式查看调用序列,快速定位状态不一致点。
技巧3:模拟磁盘损坏进行健壮性测试
在allocate_block()中随机注入错误:
if (rand() % 100 < 5) { // 5%概率返回失败
errno = ENOSPC;
return 0;
}
观察系统是否优雅降级(如提示“磁盘空间不足”而非崩溃),这能逼出隐藏的空指针解引用bug。
技巧4:路径解析的“..”陷阱
cd ..必须回溯到父目录,但根目录的父目录是谁?我们的设计是:用户根目录的parent_block_no = 0,cd ..到根目录后再次执行,自动停留在根目录(不报错)。这个逻辑在change_directory()函数中有专门判断:
if (target_block_no == 0) {
printf("Already at root directory.\n");
return;
}
很多学生直接写cwd_block_no = parent_block_no导致越界访问,加这行防御性判断就能避免。
6. 报告与任务书使用指南:如何把代码变成高分文档
配套的《操作系统课程设计任务书.doc》和《os实验报告书.docx》不是摆设,而是紧扣教学评估要点设计的“答案模板”:
-
任务书明确列出三项硬性指标:
① 多用户隔离(必须展示user1创建的文件user2不可见);
② 两级目录(必须有cd .. 和 cd /user2/ 的对比截图);
③ 读写保护(必须有user1以只读open后write失败的日志);
这三项在答辩时会被助教逐条核对,缺一项直接扣20分。 -
实验报告书的“设计目标”章节,要求用表格对比真实UNIX系统与本实现的异同:
| 特性 | UNIX真实系统 | 本课设实现 | 差异原因 |
|------|--------------|-------------|-----------|
| 目录结构 | 多级树形 | 用户根目录+当前目录 | 教学简化,降低路径解析复杂度 |
| 权限模型 | rwxr-xr-x三组 | owner/group/others八进制 | 完全兼容POSIX语义,便于知识迁移 |
| 磁盘管理 | LVM/EXT4日志 | 内存位图 | 避免引入文件系统驱动概念,聚焦核心算法 |
最后分享一个小技巧:报告中的“测试用例”章节,不要只写“输入xxx,输出yyy”。按以下格式写,助教一眼看出你真跑通了:
测试用例TC-07:跨用户权限隔离
步骤:1. login user1 123 → 2. create secret.txt → 3. logout → 4. login user2 456 → 5. dir
预期:不显示secret.txt
实际:$ dir输出为空行
截图:附命令行窗口截图(含时间戳),箭头标出关键行
这套材料的价值,不在于它有多炫酷,而在于它把操作系统课本里那些抽象概念——FCB、目录项、位图、权限位、路径解析——全都钉死在具体的内存地址、结构体字段和if语句里。你调试时看到disk[102*512+16]的值从0变成1,那一刻就真的懂了什么叫“磁盘块分配”。这比背一百遍定义都管用。
简介:北京林业大学操作系统课程设计成果,用标准C++从零实现一个支持多用户的命令行文件系统。核心功能包括用户登录、目录浏览、文件创建/删除、打开/关闭、读写操作,采用两级目录结构,每个用户拥有独立命名空间,文件信息显示物理地址、属性、长度及读写保护状态。底层模拟磁盘空间,通过内存数组实现块管理,配合用户表、目录项和文件控制块等数据结构完成权限隔离与资源调度。配套提供完整课程设计任务书和实验报告书,涵盖设计目标、整体架构、关键算法(如命令解析流程、路径查找逻辑)、磁盘映射机制及测试用例说明。扩展部分实现类Windows CMD风格交互界面,支持cd、dir、mkdir、more、copy、rmdir、find、attrib、del、xcopy、exit等常用指令,并通过import/export命令打通虚拟磁盘与本地文件系统,实现双向文件导入导出。全部代码在Windows平台编译运行,不依赖第三方库,适合课程作业参考、教学演示及文件系统原理入门实践。
6503



被折叠的 条评论
为什么被折叠?



