ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

二级文件系统课设:目录项、位图分配与g++移植实战

2026/9/18 11:44:22 拓冰建站 浏览量
二级文件系统课设:目录项、位图分配与g++移植实战 简介这份资源是一份操作系统课程设计报告主题为 Linux 二级文件系统设计面向计算机、软件工程等专业需要完成操作系统课程设计或文件系统实验的本科生与自学者。报告围绕一个简单多用户文件系统的实现展开涵盖设计目的、功能需求、实验环境、设计思路、数据结构与主要函数等章节可帮助读者理解文件系统的基本功能与内部实现机制。压缩包内共 1 个 doc 文档约 98KB为完整报告正文含 login、dir、create、delete、open、close、read、write、cd 等命令的设计说明以及 OSFILE、OSUFD、OSUFD_LOGIN、OSUFD_OPENMODE 等结构体定义与 LoginF、CreateF、DirF 等函数说明并附有两级目录结构、物理地址分配与文件读写保护的思路。目前已有 1091 人学习适合作为课程设计参考、实验报告模板与编程实现的对照资料。1. 二级文件系统的目录语义与磁盘布局拿这个课设当练手项目的人十有八九卡在同一个地方九条命令单独跑都能过凑在一起就互相打架——dir 刚列完的目录换个用户登录再列文件名串了delete 删掉一个文件下一次 create 分到的物理地址又跟别人撞上了。问题不在命令写得多烂而在有没有先把三层边界划清楚第一级目录项是用户账号第二级目录项是用户名下的文件打开表只活在内存里物理文件按编号落在磁盘上。把这套模型摊开看它其实就是 Linux 里「目录项 → inode → 数据块」的极简翻版。理解了 ufdname fname 的两级映射再回头看ls -li输出的 inode 号、stat打出来的块数、/proc/pid/fd里的一串文件描述符会顺畅很多。这个设计适合三类人刚学完操作系统进程与文件章节要交课设的、想用一个两三百行的小程序把「目录、打开表、位图分配」串起来理解的、以及准备从 Windows 下的 DevCpp 往 Linux 上搬代码练手的。后面几章按数据结构、命令实现、校验链、移植验证的顺序往下走每段都能直接抄去改。2. 从 OSFILE 到 OSUFD结构体分层与物理块编号2.1 四个结构体各管什么课设给了四个结构体很多人复制粘贴完就开始写函数写到 write 才发现flength到底该在哪个结构体上更新。先看职责表结构体关键字段生命周期是否落盘OSFILEfpaddr/flength/fmode/fname跟随目录项随目录落盘OSUFDufdname/ufdfile[MAXCHILD]跟随用户落盘为 ufd 文件OSUFD_LOGINufdname/ufdpword[8]登录期校验落盘为账号表OSUFD_OPENMODEifopen/openmode进程运行期不落盘分界线很清楚OSFILE是持久元数据描述文件本身长什么样OSUFD_OPENMODE是易失状态描述「本次运行中这个文件被谁以什么模式打开着」。把ifopen塞进OSFILE落盘是新手最常见的错误——程序退出再进来文件还显示「已打开」delete 永远删不掉。2.2 物理地址为什么用编号而不是指针课设的取巧点在于把文件内容写成file0.txt、file1.txt……文件名里的数字就是物理地址fpaddr。这样「物理地址」不落盘也能靠文件名反推位图只需一个整型数组记录哪些编号被占了。/* 物理块位图下标即块号1 表示已分配 */ int fpaddrno[MAXBLOCK]; /* 找一个空闲块号找不到返回 -1 */ int FindPANo() { for (int i 0; i MAXBLOCK; i) if (fpaddrno[i] 0) /* 0 空闲 */ return i; return -1; } /* RorW: 0 申请置 11 释放置 0 */ void SetPANo(int RorW) { int no FindPANo(); if (RorW 0 no 0) { fpaddrno[no] 1; fprintf(stdout, Alloc block %d\n, no); } }逻辑上要注意两点。一是FindPANo只负责查找、不负责占用调用方拿到编号后必须立刻fpaddrno[no] 1否则连续两次 create 会分到同一个块号——这是课设里「文件内容互相覆盖」的根因。二是SetPANo(1)在示例代码里根本没有传入要释放的块号释放通常得另写一个FreePANo(int no)delete 时按OSFILE.fpaddr精确回收。2.3 用户目录怎么持久化主目录和子目录都当文件存路径建议用常量集中管理别在十几个函数里硬编码#define DIR_ROOT c:\\osfile\\ufd\\ #define FILE_ROOT c:\\osfile\\file\\ /* 把某个用户的 OSUFD 整体写回磁盘 */ int SaveUFD(int idx) { char path[260]; sprintf(path, %s%s.ufd, DIR_ROOT, ufd[idx]-ufdname); FILE *fp fopen(path, wb); if (!fp) return -1; /* 只写有效文件数 有效项避免把数组尾部垃圾写进去 */ fwrite(fcount[idx], sizeof(int), 1, fp); fwrite(ufd[idx]-ufdfile, sizeof(OSFILE), fcount[idx], fp); fclose(fp); return 0; }参数说明idx是用户在内存数组里的下标由ExistD(ufdname)返回fcount[idx]记录该用户当前有效文件数。只写fcount[idx]条而不是整个MAXCHILD条既省空间也避免下次读回来时尾部残留的旧文件名造成「幽灵文件」。对应的LoadUFD按同样顺序读回两边字段顺序必须严格一致这是二进制序列化最容易被改崩的地方。3. 命令分发与 create/open/write 的关键路径3.1 主循环为什么用分发表而不是 if 链九条命令外加 help、cls、exit写成if/else if链能跑但加一条命令要翻半屏。用一张字符串到函数指针的表加命令只改一行typedef void (*CmdFn)(void); struct CmdEntry { const char *name; CmdFn fn; }; static struct CmdEntry cmdtab[] { {login, LoginF}, {dir, DirF}, {create, CreateF}, {delete, DeleteF}, {open, OpenF}, {close, CloseF}, {read, ReadF}, {write, WriteF}, {cd, CdF}, {attrib, ModifyFM}, {cls, clrscr}, {help, help}, {NULL, NULL} }; void Dispatch(char *line) { ltrim(rtrim(line)); /* 去掉首尾空白防 dir 匹配失败 */ for (int i 0; cmdtab[i].name; i) if (strcmp(line, cmdtab[i].name) 0) { cmdtab[i].fn(); return; } printf(Unknown command: %s\n, line); }ltrim/rtrim这两个工具函数看着不起眼但它们决定了gets读进来的open \n能不能匹配上open。同时必须先把命令名和后缀参数分离——课设的实现让每条命令自己在函数内部再gets一次参数好处是简单坏处是无法支持dir /w这种带参数的调用属于已知取舍。3.2 create先占块号还是先写文件顺序不能反。先写文件再占块号一旦块号分配失败就留个孤儿文件先占块号再写文件写失败时回滚块号即可。void CreateF() { char fname[MAXNAME]; int k ExistD(username); /* 当前用户的内存下标 */ if (k 0) { printf(Please login first\n); return; } if (fcount[k] MAXCHILD) { printf(User dir full\n); return; } printf(Please input FileName:); gets(fname); ltrim(rtrim(fname)); if (ExistF(fname) 0) { /* 重名直接拒绝别覆盖 */ printf(Error: %s already exists\n, fname); return; } int no FindPANo(); /* 1. 申请物理块号 */ if (no 0) { printf(Disk full\n); return; } fpaddrno[no] 1; /* 立刻占用别等下一步 */ int i fcount[k]; /* 2. 在目录尾部追加一项 */ ufd[k]-ufdfile[i].fpaddr no; ufd[k]-ufdfile[i].flength 0; ufd[k]-ufdfile[i].fmode 2; /* 默认读写 */ strcpy(ufd[k]-ufdfile[i].fname, fname); fcount[k]; /* 3. 建立空的物理文件路径里的编号就是 fpaddr */ char path[260]; sprintf(path, FILE_ROOT file%d.txt, no); FILE *fp fopen(path, wb); if (fp) fclose(fp); SaveUFD(k); printf(Create %s ok, phy addr %d\n, fname, no); }三个顺序点占块号 → 登记目录项 → 建物理文件任何一步失败都要把前面做的回滚掉否则位图里会出现「已占用但没有目录项」的泄漏块。3.3 open/close打开表状态机openmode用了四个值ifopen只有两个值组合起来就是一张状态机ifopenopenmode语义readwritedelete04示例中为初始已关闭拒绝拒绝允许10只读打开允许拒绝拒绝11只写打开拒绝允许拒绝12读写打开允许允许拒绝13保护态允许拒绝拒绝close 时把ifopen0、openmode4这两个赋值必须成对出现。只改ifopen不改openmode下次 open 时如果代码里写了「openmode 非 3 就沿用上次模式」的逻辑就会莫名其妙带着旧权限进来。3.4 write追加与覆盖的长度账写文件最容易算错的是长度。示例代码里先用ab追加、读完用户输入后再判断是否覆盖逻辑绕但结果对。更清晰的写法是先定模式再打开printf(Mode [0-Cover] [1-Append]: ); int choice; scanf(%d, choice); char path[260]; sprintf(path, FILE_ROOT file%d.txt, ufd[k]-ufdfile[i].fpaddr); /* 覆盖用 wb文件长度归零追加用 ab */ FILE *fp fopen(path, choice 0 ? wb : ab); if (!fp) { printf(Open physical file failed\n); return; } int len WriteF1(fp); /* 写内容返回本次写入字节数 */ if (choice 0) ufd[k]-ufdfile[i].flength len; /* 覆盖长度等于本次写入量 */ else ufd[k]-ufdfile[i].flength len; /* 追加长度累加 */ fclose(fp); SaveUFD(k);wb和ab的区别不只是「从头写」和「从尾写」——wb会先把文件截断为 0覆盖后文件里绝不残留旧内容如果错用rb新内容短于旧内容时尾部会留一段垃圾数据flength也会和实际文件大小对不上。4. 权限校验链与目录一致性delete/write 的四类坑4.1 删除前必须过三关DeleteF 的流程里连着三个 return 分支顺序有讲究/* 关卡一必须处于用户自己的目录下防止跨用户删文件 */ if (strcmp(strupr(dirname), strupr(username)) ! 0) { printf(Only your own dir\n); return; } /* 关卡二文件被打开时禁止删除避免句柄悬空 */ if (ifopen[k][i].ifopen 1) { printf(%s is open, close it first\n, fname); return; } /* 关卡三保护态fmode 3禁止删除 */ if (ufd[k]-ufdfile[i].fmode 3) { printf(%s is protected\n, fname); return; }第一关用dirname而不是username是因为cd ..之后当前目录会变必须在函数内部重新比对不能依赖登录时缓存的变量。第二关的意义在真实系统里对应「文件有引用计数就不能 unlink」这跟 Linux 允许删除已打开文件inode 延迟释放的做法不同课设选了更保守的策略。4.2fcount[k--]这类「看着对」的写法原始代码里有一行fcount[k--];编译能过、运行不报错但什么都没做。它等价于k k - 1;fcount数组整体没动。结果就是删完文件个数没减下次 create 会往已删除的槽位写入dir 列出来多一条空名记录。/* 错误只让下标 k 自减fcount 数组原封不动 */ fcount[k--]; /* 正确让该用户的文件计数减一 */ fcount[k]--; /* 更稳先取下标再减避免后续索引错位 */ int idx k; fcount[idx]--;同类问题还有数组前移的边界。for (j i; j fcount[k]; j) ufd[k]-ufdfile[j] ufd[k]-ufdfile[j1];里当j fcount[k]-1时会读到ufdfile[fcount[k]]属于越界读一个无效项。边界应该收紧一位for (j i; j fcount[k] - 1; j) ufd[k]-ufdfile[j] ufd[k]-ufdfile[j 1]; memset(ufd[k]-ufdfile[fcount[k] - 1], 0, sizeof(OSFILE)); /* 清空尾部 */ fcount[k]--;顺序不能反先做数组前移再把计数减一。反过来先减一前移循环的终止条件就少遍历一项最后一个文件会被重复保留一份。4.3 块号回收与物理文件删除的先后delete 里fpaddrno[fpaddrno1] 0;和remove(path)是两件事。正确顺序是先remove物理文件成功后再释放位图。反过来的话一旦remove失败路径不对、文件被占用块号已经被标记为空闲下一个 create 会拿到同一个编号两个目录项指向同一个fileN.txt。int err remove(path); if (err ! 0) { printf(Remove %s failed, block kept\n, path); return; /* 位图不动保持一致性 */ } fpaddrno[fpaddrno1] 0; /* 物理文件确实没了才回收块的 */ printf(%s deleted, freed block %d\n, fname, fpaddrno1);4.4 cd 与 ExistF 的路径前缀问题cd改变目录后ExistF、ExistD是按名字查的不涉及路径所以跨目录查找仍然能命中——这不是 bug而是两级目录只有两层没有更深的层级可钻。真正要防的是cd ..把dirname置空后strupr(dirname)与strupr(username)比对失败所有写操作返回「请确认您在用户目录下」。可以在CdF里对..做一次特判if (strcmp(arg, ..) 0) { strcpy(dirname, ); /* 回到根dir 时列出所有用户 */ printf(Now at root\n); } else if (ExistD(arg) 0) { strcpy(dirname, arg); printf(Now at %s\n, dirname); } else { printf(No such dir: %s\n, arg); }5. 从 DevCpp 迁到 g兼容层写法与 strace 验证落盘行为课设代码是在 DevCpp MinGW 下写的直接g osfs.cpp在 Linux 上会连环报错itoa不是标准函数、strupr是 Windows 专有、clrscr来自conio.h、gets在 C14 后已被移除。别去逐个改业务函数加一个兼容层头文件把差异挡在外面更省事。/* linux_compat.h —— 把 DevCpp 时代的专用函数补齐 */ #include cstdio #include cstring #include cstdlib #include cctype #include termios.h #include unistd.h static inline char *itoa(int v, char *buf, int base) { if (base 10) snprintf(buf, 16, %d, v); else snprintf(buf, 16, %x, v); return buf; } static inline char *strupr(char *s) { for (char *p s; *p; p) *p (char)toupper((unsigned char)*p); return s; } static inline void clrscr() { fputs(\033[2J\033[H, stdout); } static inline void InputPW(char *password) { struct termios oldt, newt; tcgetattr(STDIN_FILENO, oldt); newt oldt; newt.c_lflag ~ECHO; /* 关掉回显 */ tcsetattr(STDIN_FILENO, TCSANOW, newt); int i 0; char ch; while ((ch getchar()) ! \n i 7) { password[i] ch; putchar(*); } password[i] \0; tcsetattr(STDIN_FILENO, TCSANOW, oldt); putchar(\n); }InputPW用termios关回显效果等同于 Windows 下的getch加星号回显而且不依赖任何第三方库。clrscr用 ANSI 转义序列比system(clear)少一次 fork在循环里调用不会卡顿。路径常量也要换。Linux 没有盘符建议把根目录放到/var或用户家目录下#define DIR_ROOT /var/osfs/ufd/ #define FILE_ROOT /var/osfs/file/数据落盘是否真的按预期走别靠 printf 猜。用 strace 过滤文件系统调用能看到每一次openat的路径和标志位# 先建好目录并保证当前用户可写 sudo mkdir -p /var/osfs/{ufd,file} sudo chown -R $USER /var/osfs g -stdc11 -o fs osfs.cpp strace -f -e traceopenat,write,unlink ./fs 21 | grep osfs典型的正确输出是 create 时出现openat(AT_FDCWD, /var/osfs/file/file3.txt, O_WRONLY|O_CREAT|O_TRUNC, 0666)delete 时出现unlink(/var/osfs/file/file3.txt)。如果 write 阶段看到的是O_WRONLY|O_CREAT|O_APPEND而你在界面上选的是覆盖说明wb被写成了ab如果 delete 后没有unlink只有位图变更说明回收顺序写反了。再配合ls -l /var/osfs/file/和od -c /var/osfs/ufd/用户名.ufd核对二进制目录文件fcount和OSFILE数组的内容一眼就能看出来。权限位也能顺手验证chmod 0444 /var/osfs/file/file3.txt之后再跑 write如果程序仍然写成功说明「保护态」只在校验层拦物理文件权限没同步——这也是课设代码里值得补上的一处。本文还有配套的精品资源点击获取