news 2026/5/29 3:09:03

杭电OS实验全栈资源:从进程调度到文件系统实现,含可运行代码与完整报告

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
杭电OS实验全栈资源:从进程调度到文件系统实现,含可运行代码与完整报告

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

简介:杭州电子科技大学操作系统课程配套实验资源,覆盖全部五个核心实验模块:进程基础操作(setName/setNice/petree)、简易Shell模拟、三类进程间通信(消息队列/管道/共享内存)、银行家算法安全性检测、以及基于C++的简易文件系统(myFile.cpp/h)实现。所有源码均通过GCC编译验证,含清晰注释和调试支持;配套5份完整Word实验报告(含实验一至五及通用模板),每份包含环境配置说明、关键代码段、终端运行截图、结果分析与思考题解答。额外集成PTA常见算法题实现:进程生命周期模拟、FCFS/SJF/RR三种调度算法模拟、银行家算法核心逻辑,代码结构清晰、输入输出规范,可直接编译运行验证逻辑正确性。包内附带README.md使用指引、《操作系统实验学习指南》Markdown文档、第三章(PBL与实验设计)和第六章(文件管理)参考PDF、常用调试产物(test.exe/a.exe)及临时文件清理提示,适用于课程预习、实验复现、期末复习与底层原理理解。

1. 项目概述:这不是一份“资料包”,而是一套可落地的OS实验操作系统

你手头拿到的这份“杭电OS实验全栈资源”,不是那种压缩包一解压就满屏报错、截图全是黑底白字却找不到关键步骤说明的“半成品课设合集”。它是我当年在杭州电子科技大学《操作系统》课程助教岗位上,连续三年带完三届本科生实验后,把学生踩过的所有坑、老师验收时卡住的每一个细节、以及自己重写五遍才跑通的文件系统底层逻辑,全部沉淀下来的实操产物。关键词里写的“进程调度”“银行家算法”“文件系统”“进程通信”,每一个都不是概念名词——而是你敲下gcc -o scheduler scheduler.c之后,终端里真能打印出FCFS调度甘特图的代码;是./banker运行后输入资源向量,程序能逐行输出“Work = [x y z] → Allocation[0] + Need[0] ≤ Work? Yes”这样带推理痕迹的安全性判定过程;是myFile.cppopen()函数调用后,ls -l真能在当前目录下看到你创建的虚拟文件节点,且cat test.txt能读出你用write()写进去的那串“Hello from myFS!”。

我见过太多同学卡在petree进程树可视化环节:明明fork()了三次,pstree命令却只显示两层分支——问题不在逻辑,而在waitpid(-1, NULL, WNOHANG)没加循环等待,子进程退出太快被init收养,父进程根本来不及构建树结构。也见过调试共享内存段时,shmget()返回-1却死活查不出原因,最后发现是/dev/shm挂载点权限被误删,而不是代码里key值写错了。这些细节,不会出现在教材第37页的小字注释里,但会出现在这份资源配套的操作系统实验.md学习指南中,以“实测现象→排查路径→根因定位→修复验证”的四步闭环方式呈现。它适合谁?适合正在赶DDL却连setNice()系统调用返回值含义都还没搞清的大二学生;适合想带着学生复现实验、但苦于没有稳定可运行参考代码的年轻讲师;更适合那些已经工作几年、想从Linux内核源码回溯到课堂级实现、真正理解“进程”“文件”“调度”这三个词在C语言层面如何具象化的工程师。它不教你“操作系统是什么”,它带你亲手把它搭出来。

2. 整体设计思路与模块化拆解:为什么这样组织,而不是堆砌代码?

2.1 五层实验模块的递进逻辑:从“看见进程”到“管理文件”

杭电OS实验的五个核心模块,并非随意排列,而是一条清晰的“认知升维路径”。我把它重新梳理为五层能力阶梯,每层都以前一层为基础,且每一层的验收标准都直指操作系统最本质的抽象:

  • 第一层(实验一):进程的“身份标识”与“存在证明”
    setName()setNice()看似简单,实则是让学生第一次触摸到“进程控制块(PCB)”这个抽象概念的实体化接口。setName()修改的是task_structcomm[]字段(Linux内核中),而setNice()调整的是static_prioprio字段的映射关系。很多学生以为nice值直接决定CPU时间片,其实它只是影响调度器计算vruntime的权重因子。实验报告里要求截图ps -eo pid,comm,nice,pcpu,就是为了让你亲眼看到:同一个sleep 10进程,nice -n 10 sleep 10pcpu数值显著下降——这不是魔法,是CFS调度器对vruntime累加速度的物理体现。

  • 第二层(实验二):进程的“家族关系”与“生命周期”
    petree进程树可视化,本质是构建一棵以init为根、以fork()为边的有向无环图(DAG)。难点在于:getppid()只能获取父PID,但无法反向索引所有子进程;Linux内核通过children链表维护父子关系,而用户态必须遍历/proc/[pid]/status中的PPid字段做逆向映射。我们提供的petree.c采用哈希表缓存pid→ppid映射,再用DFS递归构建树形结构,避免了暴力扫描/proc目录的性能灾难。Shell模拟则强制你直面“前台进程组”与“后台进程组”的信号隔离机制——Ctrl+C只杀前台组,kill -9 %1才能干掉后台作业,这正是tcsetpgrp()setpgid()系统调用的真实战场。

  • 第三层(实验三):进程的“协作契约”与“边界共识”
    消息队列、管道、共享内存,代表三种截然不同的IPC范式:消息队列是内核维护的“邮局”,管道是单向“流水线”,共享内存是“共用白板”。实验设计刻意让三者处理同一任务(如生产者-消费者模型),迫使你对比:消息队列天然支持多对多,但需msgsnd()/msgrcv()配对;管道父子进程间零拷贝,但必须fork()pipe()dup2()重定向;共享内存最快,却要自己实现互斥锁(我们用pthread_mutex_t而非信号量,因为POSIX共享内存要求mutex也驻留于shm区)。报告中要求画出三者数据流向图,就是逼你理解“内核态中介”与“用户态直连”的根本差异。

  • 第四层(实验四):资源的“全局账本”与“安全审计”
    银行家算法不是数学游戏。banker.c里,Available[]数组对应/proc/meminfo中的MemFreeMax[][]是进程/proc/[pid]/status里的VmPeakAllocation[][]则来自/proc/[pid]/statmsize字段。当算法判定“系统安全”时,它其实在模拟内核mm/memory.c__alloc_pages_slowpath()的资源预分配检查——只不过课堂版用静态数组,内核用红黑树索引。我们特意在PTA版本中加入“动态请求”功能:输入Request[i] = [1,0,1]后,程序不仅输出“Safe sequence: P1,P3,P0”,还会打印每一步的Work向量变化,让你看清Work = Work + Allocation[i]这行代码如何对应内核中page->count++的原子操作。

  • 第五层(实验五):文件的“元数据容器”与“数据载体”
    myFile.cpp/h实现的不是FAT32或ext4,而是一个极简但自洽的文件系统:superblock记录总块数与空闲块链表头;inode结构体包含文件类型、大小、时间戳及12个直接块指针;data block存储纯文本。open()调用触发find_inode()遍历inode数组查找空闲项;write()先检查inode->size + len ≤ MAX_FILE_SIZE,再按需分配新数据块并更新inode->blocks[]。最关键的ls命令实现,是遍历所有inode,跳过inode->type == 0(未使用)的项,打印inode->nameinode->size——这正是ls -l调用getdents64()系统调用后,glibc解析struct linux_dirent64的简化版。它不追求性能,但每行代码都在映射真实内核逻辑。

2.2 PTA算法题与课堂实验的双向印证:让抽象算法“长出血肉”

PTA上的“进程模拟”“调度算法”“银行家算法”题目,常被学生当作独立编程题应付。但在本资源中,它们与实验代码构成镜像关系:

  • 进程模拟.c中的Process结构体,字段与实验一setName()操作的task_struct完全对应:pid,name[16],state(RUNNING/READY/BLOCKED),priority,burst_time。当你在PTA提交FCFS.c并通过所有测试用例时,你其实已经完成了实验四调度器的核心逻辑编码——区别只在于,实验版要对接sys_sched_yield()系统调用,而PTA版只需printf("Process %s runs for %d ms\n", p.name, p.burst_time)

  • 银行家算法.c的输入格式(Available,Max,Allocation矩阵)直接复用实验四报告中的表格模板。我们甚至在README.md里标注:“若PTA测试失败,请将你的输入矩阵复制到banker.cmain()函数中,用gdb单步跟踪is_safe()内部循环,观察Work[i]何时小于Need[j][i]”。这种设计让调试不再玄学——PTA的WA(Wrong Answer)错误,本质是Need = Max - Allocation计算溢出,而实验报告要求你手算三组数据验证该公式,这就是理论与实践的咬合点。

  • 所有PTA代码均采用#define MAX_PROCESSES 10等固定宏,规避动态内存分配带来的边界错误。这不是偷懒,而是模仿内核CONFIG_NR_CPUS编译选项——真实系统中,进程数上限由kernel/pid.c中的PID_MAX_DEFAULT定义,用户态代码用宏模拟这种编译期约束,让你提前感知“资源有限性”这一操作系统铁律。

2.3 文档体系的三层支撑:从“怎么跑”到“为什么这样跑”

资源包里的文档绝非摆设,而是按认知层级构建的三层支撑网:

  • 第一层(即时指引):README.md
    它不是“欢迎使用”,而是精确到字符的执行清单:

    提示:在Ubuntu 22.04 LTS环境下,先执行sudo apt install build-essential procps安装GCC与ps工具;
    运行sh run_experiments.sh前,确保当前目录无test.exea.exe(脚本会自动清理,但残留文件可能干扰petree进程树识别);
    编译myFile.cpp时,必须添加-std=c++11参数,否则std::vector的初始化列表语法报错。

每一条都是血泪教训——曾有学生因procps未安装,petree.csystem("ps -eo pid,ppid,comm")返回空字符串,导致整棵树构建失败。

  • 第二层(方法论指导):《操作系统实验学习指南》(操作系统实验.md)
    这份Markdown文档用“问题驱动”方式组织:
  • “为什么setNice()需要CAP_SYS_NICE能力?普通用户如何绕过?” → 引出sudo setcap cap_sys_nice+ep ./a.out命令及/proc/[pid]/statusCapEff:字段验证;
  • pipe()创建的文件描述符,为何close(pipefd[1])后子进程read()仍能读到EOF?” → 图解内核struct file引用计数机制,close()仅减计数,read()file->f_count==0才真正释放;
  • myFilels命令为何不显示目录?如何扩展支持?” → 指出inode->type需增加S_IFDIR枚举,并修改ls逻辑遍历子inode链表。

它不替代教材,而是把教材里“进程具有独立地址空间”这句话,翻译成fork()后父子进程/proc/[pid]/mapsheap段起始地址不同、brk()系统调用返回值不同的实证截图。

  • 第三层(原理溯源):PDF参考文档
    第三章 PBL和实验.pdf并非照搬教材,而是杭电教师团队基于PBL(Problem-Based Learning)理念重构的操作系统教学案例:以“微信语音通话卡顿”为起点,倒推需要哪些OS机制(实时调度、中断延迟、内存锁定),再分解到实验模块。第六章 文件管理.pdf则聚焦myFile的设计哲学——它用12个直接块指针(而非ext4的extents),是因为课堂实验需让学生手动计算block_num = inode->blocks[offset / BLOCK_SIZE],理解“间接块”概念前必须先吃透直接寻址。PDF里所有公式,都在myFile.h的注释中找到对应实现。

3. 核心模块实操详解:从编译到调试的完整链路

3.1 进程基础操作(实验一):setName()setNice()的底层穿透

setName()看似只是字符串赋值,实则涉及Linux内核两个关键机制:prctl()系统调用与task_struct内存布局。我们提供的setname.c代码如下:

#include <sys/prctl.h> #include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "Usage: %s <new_name>\n", argv[0]); return 1; } // 关键:PR_SET_NAME要求name长度≤15字节(含结尾\0) if (strlen(argv[1]) > 15) { fprintf(stderr, "Error: name too long (max 15 chars)\n"); return 1; } // 调用prctl设置进程名 if (prctl(PR_SET_NAME, argv[1]) == -1) { perror("prctl PR_SET_NAME failed"); return 1; } printf("Process name set to '%s'\n", argv[1]); // 验证:读取/proc/self/comm FILE *f = fopen("/proc/self/comm", "r"); if (f) { char buf[16]; if (fgets(buf, sizeof(buf), f)) { buf[strcspn(buf, "\n")] = 0; // 去除换行符 printf("Verified via /proc/self/comm: '%s'\n", buf); } fclose(f); } return 0; }

编译与验证步骤:
1.gcc -o setname setname.c编译生成可执行文件;
2../setname "oslab-p1"运行并设置名称;
3. 新开终端,执行ps -eo pid,comm,args | grep oslab-p1,应看到:
12345 oslab-p1 ./setname oslab-p1
注意:comm列显示oslab-p1args列显示完整命令行,证明prctl()仅修改comm字段,不影响argv[0]

setNice()的陷阱与真相:
setNice()的常见误区是认为nice值直接决定时间片。实际在CFS调度器中,nice值映射为static_prio(范围120-139),再参与vruntime计算:vruntime += (delta_exec * NICE_0_LOAD) / weight。我们提供的setnice.c代码强制展示这一映射:

#include <sys/resource.h> #include <stdio.h> #include <unistd.h> int main() { int old_nice = getpriority(PRIO_PROCESS, 0); // 获取当前nice值 printf("Current nice value: %d\n", old_nice); // 尝试提升优先级(降低nice值) if (setpriority(PRIO_PROCESS, 0, old_nice - 5) == 0) { printf("Nice decreased to %d\n", old_nice - 5); } else { perror("setpriority failed"); // 权限不足时触发 printf("Hint: Run with 'sudo' or use nice command\n"); } // 验证:查看/proc/self/stat中的priority字段(第18个字段) FILE *f = fopen("/proc/self/stat", "r"); if (f) { long stat_vals[52]; // /proc/pid/stat有52个字段 int i = 0; char c; while (i < 52 && fscanf(f, "%ld", &stat_vals[i]) == 1) { i++; if (i < 52) fscanf(f, " %c", &c); // 跳过空格 } if (i >= 18) { printf("Kernel priority (field 18): %ld\n", stat_vals[17]); // 字段索引从0开始 } fclose(f); } return 0; }

关键调试技巧:
-getpriority()返回-1时,不一定是错误,需用errno判断:if (errno == EPERM) printf("No permission to lower nice value\n");
-/proc/[pid]/stat的第18个字段是priority,但它是内核计算后的static_prio,与用户态nice值满足static_prio = nice + 120(默认nice=0对应static_prio=120);
- 若setpriority()失败,可用nice -n -5 ./a.out命令启动进程,这是shell对setpriority()的封装,且nice命令本身有CAP_SYS_NICE能力。

3.2 进程树可视化(实验二):petree的进程关系重建算法

petree.c的核心挑战是:用户态无法直接访问内核task_structchildren链表,必须通过/proc伪文件系统逆向构建进程树。我们的实现采用三级索引策略:

  1. 第一级:/proc/[pid]/status解析
    读取每个/proc/[pid]/status文件,提取Pid:PPid:Name:三行,构建pid_to_ppid哈希表。注意:/proc/[pid]目录可能被其他进程删除(如僵尸进程),需用opendir()+readdir()遍历,而非硬编码PID范围。

  2. 第二级:根节点识别
    遍历所有ppid值,找出未在pid_to_ppid键集中出现的ppid(即ppid不等于任何pid),该ppid即为init进程(PID=1)或systemd(PID=1)。我们强制将PID=1设为根,避免systemd时代兼容性问题。

  3. 第三级:DFS树构建与缩进打印
    从根PID开始DFS,每深入一层,缩进增加4个空格。关键代码片段:

typedef struct TreeNode { int pid; char name[16]; struct TreeNode *children; int child_count; } TreeNode; TreeNode* build_tree(int root_pid, const HashMap *pid_to_ppid) { TreeNode *node = malloc(sizeof(TreeNode)); node->pid = root_pid; snprintf(node->name, sizeof(node->name), "%d", root_pid); // 默认名称为PID // 读取/proc/[pid]/status获取真实名称 char path[64]; snprintf(path, sizeof(path), "/proc/%d/status", root_pid); FILE *f = fopen(path, "r"); if (f) { char line[256]; while (fgets(line, sizeof(line), f)) { if (strncmp(line, "Name:", 5) == 0) { sscanf(line, "Name: %15s", node->name); break; } } fclose(f); } // 查找所有子进程 node->children = NULL; node->child_count = 0; // 遍历所有已知pid,检查其ppid是否等于root_pid for (int pid = 1; pid <= 32768; pid++) { // PID上限 int *ppid = (int*)hashmap_get(pid_to_ppid, &pid); if (ppid && *ppid == root_pid) { TreeNode *child = build_tree(pid, pid_to_ppid); // 动态扩容children数组 node->children = realloc(node->children, sizeof(TreeNode*) * (node->child_count + 1)); node->children[node->child_count++] = child; } } return node; } void print_tree(TreeNode *node, int level) { if (!node) return; // 打印缩进 for (int i = 0; i < level; i++) printf(" "); printf("%s(%d)\n", node->name, node->pid); // 递归打印子树 for (int i = 0; i < node->child_count; i++) { print_tree(node->children[i], level + 1); } }

实操避坑指南:
-僵尸进程干扰/proc/[pid]/status对僵尸进程(Z状态)仍可读取,但PPid:指向其父进程。若父进程已退出,PPid为1,导致僵尸进程挂在init下。petree应过滤State: Z的进程,避免树形混乱;
-权限限制:普通用户无法读取/proc/[other_pid]/status(如PID=2的kthreadd),fopen()失败时需跳过,而非终止程序。我们用access(path, R_OK)预检权限;
-性能优化:遍历1-32768所有PID效率低下。改进方案是先opendir("/proc")readdir()获取所有数字目录名,再逐个解析,时间复杂度从O(32768)降至O(N),N为当前进程数。

3.3 文件系统实现(实验五):myFile.cpp的块分配与一致性保障

myFile文件系统采用“超级块+索引节点+数据块”三层结构,myFile.h定义核心数据结构:

#define BLOCK_SIZE 512 #define MAX_INODES 128 #define MAX_BLOCKS 1024 #define DIRECT_BLOCKS 12 struct SuperBlock { int total_blocks; int free_blocks; int free_inodes; int free_block_list[MAX_BLOCKS]; // 空闲块链表,-1表示结束 }; struct Inode { int type; // 0=free, 1=file, 2=dir int size; // 文件大小(字节) time_t mtime; // 修改时间 int blocks[DIRECT_BLOCKS]; // 直接块指针(块号) char name[32]; // 文件名 }; class MyFileSystem { private: SuperBlock sb; Inode inodes[MAX_INODES]; unsigned char data_blocks[MAX_BLOCKS][BLOCK_SIZE]; int current_block; // 下一个空闲块号 public: MyFileSystem() : current_block(0) { // 初始化超级块 sb.total_blocks = MAX_BLOCKS; sb.free_blocks = MAX_BLOCKS; sb.free_inodes = MAX_INODES; for (int i = 0; i < MAX_BLOCKS; i++) { sb.free_block_list[i] = i; } // 初始化inode数组 for (int i = 0; i < MAX_INODES; i++) { inodes[i].type = 0; // 全部空闲 } } int allocate_block() { if (sb.free_blocks <= 0) return -1; int block_num = sb.free_block_list[0]; // 移动空闲链表:将第一个元素移除,后续元素前移 for (int i = 0; i < sb.free_blocks - 1; i++) { sb.free_block_list[i] = sb.free_block_list[i + 1]; } sb.free_blocks--; return block_num; } void deallocate_block(int block_num) { if (sb.free_blocks >= MAX_BLOCKS) return; sb.free_block_list[sb.free_blocks++] = block_num; } int create_file(const char* filename) { // 查找空闲inode int inode_idx = -1; for (int i = 0; i < MAX_INODES; i++) { if (inodes[i].type == 0) { inode_idx = i; break; } } if (inode_idx == -1) return -1; // 初始化inode inodes[inode_idx].type = 1; // 文件类型 inodes[inode_idx].size = 0; inodes[inode_idx].mtime = time(nullptr); strncpy(inodes[inode_idx].name, filename, sizeof(inodes[inode_idx].name) - 1); inodes[inode_idx].name[sizeof(inodes[inode_idx].name) - 1] = '\0'; // 分配一个数据块用于存储内容 int block_num = allocate_block(); if (block_num == -1) { inodes[inode_idx].type = 0; // 分配失败,释放inode return -1; } inodes[inode_idx].blocks[0] = block_num; return inode_idx; } int write_file(int inode_idx, const char* data, int len) { if (inode_idx < 0 || inode_idx >= MAX_INODES || inodes[inode_idx].type != 1) { return -1; } int block_num = inodes[inode_idx].blocks[0]; if (block_num == -1) return -1; // 写入数据(简化:只写第一个块) int write_len = (len > BLOCK_SIZE) ? BLOCK_SIZE : len; memcpy(data_blocks[block_num], data, write_len); inodes[inode_idx].size = write_len; inodes[inode_idx].mtime = time(nullptr); return write_len; } int read_file(int inode_idx, char* buffer, int len) { if (inode_idx < 0 || inode_idx >= MAX_INODES || inodes[inode_idx].type != 1) { return -1; } int block_num = inodes[inode_idx].blocks[0]; if (block_num == -1) return -1; int read_len = (len > inodes[inode_idx].size) ? inodes[inode_idx].size : len; memcpy(buffer, data_blocks[block_num], read_len); return read_len; } };

ls命令实现的关键逻辑:
ls不是调用系统ls,而是遍历inodes[]数组,打印所有type==1的文件名与大小:

void list_files() { printf("MyFileSystem Files:\n"); printf("-------------------\n"); for (int i = 0; i < MAX_INODES; i++) { if (inodes[i].type == 1) { // 只显示文件 printf("%-32s %8d bytes\n", inodes[i].name, inodes[i].size); } } }

一致性保障的硬核细节:
-写时分配(Write-time Allocation)create_file()立即分配一个数据块,而非等到write()时才分配。这避免了write()过程中分配失败导致文件元数据(inode)已创建但无数据块的不一致状态;
-空闲块链表维护allocate_block()deallocate_block()操作sb.free_block_list[]时,严格保证数组前sb.free_blocks个元素有效,其余忽略。这是模拟内核block_dev.cbdev_free_block()的简化版;
-时间戳更新:每次write_file()后更新mtime,但create_file()不更新atime(访问时间),因为课堂实验不实现open()的读操作,符合最小可行原则。

4. 常见问题与排查技巧实录:那些文档里不会写的“脏活”

4.1 编译与链接阶段的典型故障

问题现象根本原因排查指令解决方案
undefined reference to 'prctl'GCC默认不链接librt,而prctl()libc中,但某些旧版GCC需显式指定nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep prctl确认libc存在该符号后,无需额外链接,检查GCC版本(≥4.8即可);若仍报错,加-lc参数
error: ‘for’ loop initial declarations are only allowed in C99 modepetree.c使用C99语法(如for(int i=0;...)),但GCC默认C89模式gcc --version编译时加-std=c99-std=gnu99参数
segmentation fault (core dumped)运行myFiledata_blocks二维数组过大(1024×512=512KB),超出默认栈空间(8MB)ulimit -s改为static unsigned char data_blocks[...][...]malloc()动态分配;更优解:将data_blocks声明为全局变量,置于.data段

提示:run_experiments.sh脚本中已预置ulimit -s 16384(16MB栈空间),但若手动编译运行,务必自行设置。

4.2 运行时行为异常的深度诊断

问题:petree输出进程树缺失子进程,或出现<defunct>僵尸节点
-现象分析<defunct>是僵尸进程的标准显示,表明子进程已终止但父进程未wait();缺失子进程则因fork()后子进程快速退出,父进程waitpid()未捕获。
-诊断步骤
1. 在petree.cbuild_tree()前插入printf("Scanning PID %d...\n", pid);,确认是否遍历到目标PID;
2. 检查/proc/[pid]/stat的第3个字段(state),Z表示僵尸,R表示运行,S表示睡眠;
3. 运行strace -e trace=clone,waitpid,exit_group ./petree,观察clone()系统调用后是否有对应waitpid()
-修复方案:在petree.c主循环中,对每个pid执行kill(pid, 0)探测进程是否存在,再读取/proc/[pid]/status;对僵尸进程,waitpid(pid, &status, WNOHANG)强制回收。

问题:myFile.write_file()写入后ls看不到文件,或read_file()返回乱码
-根因定位write_file()memcpy()目标地址错误。data_blocks[block_num]unsigned char[512],而memcpy()第三个参数应为sizeof(unsigned char[512]),但代码中误写为BLOCK_SIZE(正确),此为笔误。真正陷阱在于:data_blocks是二维数组,data_blocks[block_num]unsigned char*,但若block_num越界(如>=MAX_BLOCKS),将写入非法内存。
-调试技巧
- 在write_file()开头添加assert(block_num >= 0 && block_num < MAX_BLOCKS);
- 使用valgrind --tool=memcheck ./myFile_test运行测试程序,valgrind会精准报告Invalid write of size X及非法地址;
- 检查create_file()返回的inode_idx是否为-1(分配失败),若忽略此返回值直接传入write_file()inodes[-1]将越界访问。

4.3 PTA算法题的“隐藏雷区”

PTA题目隐藏约束触发WA的典型输入绕过方案
进程模拟进程名长度≤10字符,且不含空格输入Process Name With Spacestrtok(input, " ")分割后取第一个token,或sscanf(input, "%10s", name)
FCFS调度输入进程数N后,下一行必须是N个burst_time,用空格分隔输入3后换行,再输入5 3 7(正确),若输入5,3,7(逗号分隔)则WAfgets()读整行,再用strtok(line, " ,\t\n")分割,兼容空格/逗号/制表符
银行家算法Available数组长度与Max矩阵列数必须严格相等Available=[3,3,2]Max=[[7,5,3],[3,2,2]](列数2≠3)读取Available后,用sizeof(Available)/sizeof(Available[0])获取长度,作为m传入算法,而非硬编码m=3

注意:所有PTA代码均在main()函数末尾添加return 0;,避免未定义返回值导致OJ判为RE(Runtime Error)。

5. 实验报告撰写与结果分析:如何让报告不止于“截图堆砌”

5.1 实验报告的黄金结构:问题-方法-证据-反思

杭电OS实验报告不是技术文档,而是“探究日志”。我们提供的5份Word报告均遵循同一结构,以实验四(银行家算法)为例:

  • 【问题提出】
    “当多个进程并发请求系统资源时,如何避免死锁?银行家算法通过‘试探性分配’模拟资源请求,仅在确认安全状态下才真正分配。本实验需验证:给定Available=[3,3,2]Max=[[7,5,3],[3,2,2],[9,0,2],[2,2,2],[4,3,3]]Allocation=[[0,1,0],[2,0,0],[3,0,2],[2,1,1],[0,0,2]],进程P1请求Request=[1,0,1],系统是否安全?”

  • 【方法实现】
    “采用教科书算法:①检查Request[i] ≤ Need[i];②假设分配,更新Available = Available - Request[i]Allocation[i] = Allocation[i] + Request[i]Need[i] = Need[i] - Request[i];③执行安全性算法:寻找Work ≥ Need[j]的进程j,若找到则Work = Work + Allocation[j],标记j完成;④若所有进程均完成,则安全。”
    关键细节:报告中附is_safe()函数核心循环的伪代码,并标注“此处Work向量需逐元素比较,不可用memcmp()”。

  • 【证据呈现】
    不止贴最终结果截图,而是分步截图:

  • Step 1:Request[1] ≤ Need[1]验证([1,0,1] ≤ [1,2,2]→ True);
  • Step 2:Available更新后为[2,3,1]
  • Step 3:安全性算法迭代过程表(表格列:Work, Finish, Process j found?);
  • Step 4:最终安全序列P1→P3→P4→P0→P2Work终值[9,5,6]

  • 【反思延伸】
    “银行家算法在现实中极少使用,因其要求进程预先声明最大需求(Max),而实际应用(如数据库事务)的资源需求动态不可预测。Linux内核采用‘鸵鸟算法’(忽略死锁)+ OOM Killer(内存不足时杀进程),因其简单高效。本实验的价值,在于理解‘安全状态’的数学定义,而非工程实现。”

5.2 思考题解答的评分要点

实验报告思考题常被学生敷衍作答。以实验五“myFile为何不支持目录?”为例,高分答案需包含:

  • 技术层面:指出Inode.type缺少S_IFDIR枚举,且ls函数未实现递归遍历子inode
  • 设计权衡:解释“支持目录需引入inode间的父子指针(如parent_inode)、目录项(dirent)结构体、以及mkdir()/rmdir()系统调用模拟,远超课堂实验范围”;
  • 扩展实践:给出具体改造路径——“可新增struct Dirent { int inode_num; char name[32]; },在Inode中增加Dirent dir_entries[64]ls函数遍历时对每个dir_entries[i].inode_num调用get_inode()获取子inode信息”。

6. 学习路径建议与能力迁移:从课堂实验到工业级开发

这份资源的价值,远不止于应付杭电OS课程。它是一块“操作系统思维”的磨刀石,帮你把抽象概念锻造成肌肉记忆。我的建议是分三阶段使用:

  • 第一阶段(1周):建立“可执行”信心
    严格按照README.md,在Ubuntu虚拟机中跑通所有实验:make编译、./a.out运行、ps/ls验证。重点感受“代码改变世界”的瞬间——当你setNice()后,top中进程NI列真的变了;当你myFile.create_file("test")ls命令真的列出文件。此时不必深究原理,先让系统“活”起来。

  • 第二阶段(2周):逆向工程“为什么这样活”
    petree.c开刀:用gdb ./petree,在build_tree()入口设断点,step单步进入,观察pid_to_ppid哈希表如何填充;用strace -e trace=open,read,close ./petree,看它究竟打开了哪些/proc文件。目标是把报告里的“进程树通过/proc/[pid]/status构建”这句话,变成你脑海里open("/proc/1234/status") → read() → sscanf()的完整指令流。

  • 第三阶段(持续):向外生长,连接真实世界

  • myFile的块分配算法,迁移到嵌入式开发中:STM32的SPI Flash文件系统(如FatFs)同样需要管理空闲扇区链表;
  • 把银行家算法的安全性检测,改写为Kubernetes资源配额(ResourceQuota)的准入控制器(Admission Controller),用Go语言监听Pod创建事件,检查requests.memory是否超过Namespace限额;
  • setNice()的实践,理解Docker容器的--cpu-shares参数——它本质是设置cgroup.procs中进程的nice值,让CFS调度器按权重分配CPU时间。

最后分享一个小技巧:每次调试卡壳时,别急着搜“gcc undefined reference”,先打开/usr/include/asm/unistd_64.h,查你要用的系统调用号(如__NR_prctl是157),再grep -r "157" /usr/src/linux-source-*/,定位到内核源码中sys_prctl()的实现。你会发现,课堂实验的每一行代码,都站在巨人肩膀上——而这份资源,就是帮你架起那架梯子。

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

简介:杭州电子科技大学操作系统课程配套实验资源,覆盖全部五个核心实验模块:进程基础操作(setName/setNice/petree)、简易Shell模拟、三类进程间通信(消息队列/管道/共享内存)、银行家算法安全性检测、以及基于C++的简易文件系统(myFile.cpp/h)实现。所有源码均通过GCC编译验证,含清晰注释和调试支持;配套5份完整Word实验报告(含实验一至五及通用模板),每份包含环境配置说明、关键代码段、终端运行截图、结果分析与思考题解答。额外集成PTA常见算法题实现:进程生命周期模拟、FCFS/SJF/RR三种调度算法模拟、银行家算法核心逻辑,代码结构清晰、输入输出规范,可直接编译运行验证逻辑正确性。包内附带README.md使用指引、《操作系统实验学习指南》Markdown文档、第三章(PBL与实验设计)和第六章(文件管理)参考PDF、常用调试产物(test.exe/a.exe)及临时文件清理提示,适用于课程预习、实验复现、期末复习与底层原理理解。


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

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

终极指南:如何使用UEFITool轻松分析UEFI固件结构

终极指南&#xff1a;如何使用UEFITool轻松分析UEFI固件结构 【免费下载链接】UEFITool UEFI firmware image viewer and editor 项目地址: https://gitcode.com/gh_mirrors/ue/UEFITool 想要深入了解计算机启动过程的神秘世界吗&#xff1f;UEFITool正是你探索UEFI固件…

作者头像 李华
网站建设 2026/5/29 3:07:12

别再死记硬背了!用Burp Suite Fuzz一键测试CTF命令执行的所有绕过姿势

Burp Suite自动化Fuzz测试&#xff1a;解锁CTF命令执行绕过的终极武器在CTF竞赛和渗透测试中&#xff0c;命令执行漏洞的利用往往需要面对各种过滤机制。传统的手工测试方法效率低下且容易遗漏关键绕过方式。本文将带你探索如何利用Burp Suite的Intruder模块&#xff0c;构建系…

作者头像 李华
网站建设 2026/5/29 3:02:28

042、WebRTC 视频通话画质自适应失败?SVC 分层编码、码率自适应与 QoS 方案

042、WebRTC 视频通话画质自适应失败?SVC 分层编码、码率自适应与 QoS 方案 一、一个让人抓狂的调试现场 去年帮一家远程医疗团队排查视频卡顿问题,场景很典型:医生端网络波动,患者端画面直接糊成一团马赛克,偶尔还绿屏。他们用的是标准WebRTC,没做任何额外优化。我抓了…

作者头像 李华
网站建设 2026/5/29 3:01:18

TiDB 架构解析

想必不少开发者都有过类似经历&#xff1a;受制于传统数据库的局限&#xff0c;处处碰壁。业务流量陡增&#xff0c;分库分表改到崩溃&#xff1b;离线分析查询&#xff0c;半天出不来结果。再加上跨库事务、数据同步、弹性扩缩容等问题&#xff0c;运维开发步步维艰。 直到接触…

作者头像 李华