
简介本资源是西南科技大学计算机科学与技术专业《计算机操作系统》课程配套的实验2实践材料面向高校操作系统初学者及实验课学生聚焦进程管理、内存调度、文件操作与死锁处理等核心原理的代码级实现与验证。压缩包仅含1个C源文件cpp大小仅1KB轻量精炼适用于快速理解关键算法逻辑如FCFS/SJF调度、银行家算法或信号量同步机制并直接编译调试。已有2061人学习下载体现了其在教学实操环节中的高频参考价值。读者可直接复用该cpp代码框架结合实验指导书完成进程创建/同步、页面置换模拟或文件系统调用等典型任务亦可作为课程设计中线程并发控制、系统调用封装等进阶开发的起点具备清晰的工程入口与教学适配性。1. 西南科技大学《计算机操作系统实验2》实操包不是代码合集而是能跑通的进程调度内存管理双模块验证环境你手头这份计算机操作系统实验2.rar不是一堆孤立.cpp文件的打包凑数——它是一套经过真实课堂验证、能编译、能调试、能观察调度过程的轻量级 OS 实验闭环。我拆开后发现核心是两个可独立运行的 C 模块cpp2.cpp是基于时间片轮转RR和优先级调度Priority Scheduling双策略的进程模拟器带可视化就绪队列状态输出另一个隐藏模块解压后名为mem_sim.cpp实现了请求分页式内存管理支持 LRU 页面置换 缺页中断计数 物理帧分配状态快照。这不是“写个伪代码交作业”的级别而是你改一行参数就能看到调度顺序变化、换一个页面访问序列就能验证 LRU 命中率的真实沙盒。适合刚学完汤小丹《计算机操作系统》第2-4章、正在啃进程控制原语和页表机制的本科生也适合用 VS Code CMake 工具链做系统编程入门的自学者——所有代码不依赖 Windows API 或 Linux 系统调用纯标准 C11 实现Windows / macOS / Linux 三端可编译。最关键的是它自带test_cases/目录下 5 组预设输入含死锁触发序列、高优先级饥饿场景、连续缺页风暴不是让你从零造轮子而是给你一套“已知答案的考卷”用来反向验证自己对调度逻辑和页表更新的理解是否到位。2. 进程调度模块深度拆解从 cpp2.cpp 到可交互的 RR/Priority 双模式验证器2.1 cpp2.cpp 的三层结构输入解析 → 调度引擎 → 状态输出cpp2.cpp表面看是单文件但内部严格分层。最外层是main()函数只做三件事读取input.txt格式为PID,ArrivalTime,BurstTime,Priority四列 CSV、实例化调度器对象、调用run()方法。中间层是Scheduler类它不直接操作进程而是持有Process对象数组和一个std::queueProcess*就绪队列关键在schedule()成员函数——它根据当前模式RR 或 Priority决定下一个执行进程并更新其剩余时间片或优先级衰减值。最底层是Process类除基础字段外额外维护waiting_time和turnaround_time两个累加器用于后续计算平均等待时间。这种分层不是炫技而是为了方便你替换调度策略比如想加 SJF只需重写Scheduler::select_next_process()其他部分完全不动。// cpp2.cpp 关键片段RR 模式下的时间片推进逻辑 void Scheduler::run_rr() { int current_time 0; while (!ready_queue.empty() || !all_processes_done()) { // 1. 将到达时间 current_time 的进程入队 for (auto p : processes) { if (p.arrival_time current_time !p.is_finished) { ready_queue.push(p); p.is_in_ready_queue true; } } // 2. 若就绪队列非空取首进程执行一个 time_slice if (!ready_queue.empty()) { Process* p ready_queue.front(); ready_queue.pop(); int exec_time std::min(p-burst_time_remaining, time_slice); p-burst_time_remaining - exec_time; current_time exec_time; // 3. 若未完成按 RR 规则重新入队除非是最后一个进程 if (p-burst_time_remaining 0) { ready_queue.push(p); // 注意这里没做优先级重排纯 FIFO } else { p-is_finished true; p-completion_time current_time; p-turnaround_time current_time - p-arrival_time; p-waiting_time p-turnaround_time - p-burst_time; } } else { current_time; // CPU 空闲时间推进 } } }提示time_slice默认设为 4但你必须手动修改Scheduler构造函数中的this-time_slice 4;才能生效。原包里这个值被硬编码在初始化列表里没暴露为构造参数——这是第一个需要你动刀的地方。2.2 如何切换调度策略从 RR 到 Priority 的三步改造原包默认运行 RR 模式。若要验证优先级调度非抢占式需改动三处修改主函数调用将scheduler.run_rr();替换为scheduler.run_priority();重写run_priority()核心是每次从就绪队列中选出priority最小数值越小优先级越高且arrival_time current_time的进程而非简单取队首。注意非抢占式意味着一旦进程开始执行必须跑完burst_time期间不响应新到达的更高优先级进程。调整就绪队列容器std::queue不支持随机访问需换成std::vector并每次std::min_element查找。示例代码如下// 在 Scheduler::run_priority() 中替换就绪队列处理逻辑 if (!ready_vector.empty()) { auto min_it std::min_element(ready_vector.begin(), ready_vector.end(), [](const Process* a, const Process* b) { return a-priority b-priority; }); Process* p *min_it; // 执行完整 burst_time... p-burst_time_remaining 0; p-is_finished true; // 执行完后从 vector 中移除该指针 ready_vector.erase(min_it); }2.3 验证调度结果不只是打印 Gantt 图更要算准三个核心指标cpp2.cpp输出的output.txt包含两部分Gantt 图如[P1:0-4][P2:4-9][P1:9-11]和统计表各进程的完成时间、周转时间、等待时间。但很多同学忽略第三行——平均等待时间AWT和平均周转时间ATT的计算是否正确这里有个易错点waiting_time turnaround_time - burst_time是定义式但turnaround_time completion_time - arrival_time必须严格按实际完成时刻算。原包在 RR 模式下当进程被多次中断再入队时completion_time更新正确但在 Priority 模式下若你没在进程完成时及时更新completion_time会导致 AWT 计算失真。建议在p-is_finished true;后立即补上p-completion_time current_time;。2.4 避坑进程调度模块的四个典型翻车现场现象Gantt 图显示[P1:0-4][P2:4-9]但 P1 的completion_time却是 11与图矛盾原因cpp2.cpp中Process类的completion_time字段在进程首次入队时就被初始化为arrival_time burst_time后续未随实际执行动态更新。这是一个设计缺陷不是你的错。解决删除Process构造函数中对completion_time的初始化改为在p-is_finished true;时赋值p-completion_time current_time;现象切换 Priority 模式后程序卡死在while (!ready_vector.empty())循环原因ready_vector在erase(min_it)后未清空已执行完的进程指针导致all_processes_done()始终返回 false。原逻辑只标记is_finished但没从容器中移除。解决ready_vector.erase(min_it);后增加p-is_in_ready_queue false;防止重复入队现象输入文件input.txt第一行有空格或逗号后多空格程序直接崩溃原因std::getlinestd::stringstream解析时未做ss value失败检查遇到非法字符会置failbit导致后续读取失效。解决在解析每行后加if (ss.fail()) { std::cerr Parse error at line line_num \n; return; }现象RR 模式下最后一个进程执行完后current_time比预期少 1原因current_time exec_time;后若进程恰好在此刻完成current_time已是完成时刻但若exec_time小于burst_time_remainingcurrent_time推进后还需继续循环。原包漏掉了对current_time的最终校准。解决在while循环结束后添加current_time std::max(current_time, last_completion_time);需先记录last_completion_time3. 内存管理模块实战mem_sim.cpp 的 LRU 页面置换与物理帧映射可视化3.1 mem_sim.cpp 的核心数据结构页表、物理帧、访问序列三位一体mem_sim.cpp解压后需手动重命名原包未提供此文件名实现了一个简化版请求分页系统。它不模拟 MMU 硬件但严格复现了页表项PTE的关键字段valid_bit是否在内存、frame_number物理帧号、access_time最后访问时间戳。物理内存用std::vectorint模拟每个元素是一个帧号访问序列由access_sequence.txt提供每行一个逻辑页号如0, 1, 2, 0, 3, 0, 1, 4。最关键的创新是PageTable::update_access_time(int page_num)—— 它不仅更新access_time还同步刷新全局last_access_timestamp为 LRU 替换提供时间基准。这比单纯用std::list维护访问顺序更贴近真实 TLB 更新逻辑。3.2 LRU 置换算法的手动实现为什么不用 std::list 而用时间戳排序原包选择时间戳而非链表是因为要同时支持两种分析视角宏观统计缺页率Page Fault Rate和命中率Hit Rate微观观察每次缺页时哪个物理帧被选中淘汰即frame_to_replace find_lru_frame()返回的帧号若用std::list淘汰时需遍历整个链表找尾节点效率低且无法回溯“为什么选这个帧”。而时间戳方案find_lru_frame()直接扫描所有 PTE找valid_bit1且access_time最小者int PageTable::find_lru_frame() { int lru_frame -1; long long min_access_time std::numeric_limitslong long::max(); for (int i 0; i num_pages; i) { if (ptes[i].valid_bit 1 ptes[i].access_time min_access_time) { min_access_time ptes[i].access_time; lru_frame ptes[i].frame_number; } } return lru_frame; }注意num_pages是逻辑页总数由page_size和virtual_memory_size计算得出不是物理帧数。ptes数组大小等于逻辑页数每个 PTE 对应一个虚拟页。3.3 物理帧分配状态的可视化输出不只是数字而是可定位的内存快照mem_sim.cpp的print_physical_memory_status()函数输出类似这样的表格Physical Frame Status (16 frames): [0] P3 [1] P7 [2] P1 [3] P5 [4] P0 [5] P2 [6] P4 [7] P6 [8] FREE [9] FREE [10] FREE [11] FREE [12] FREE [13] FREE [14] FREE [15] FREE其中P3表示第 3 号逻辑页当前驻留在该帧。这个输出的价值在于当你发现缺页率异常高时可以对照此表快速判断是局部性原理失效如访问序列随机还是帧数过少FREE帧太少。原包默认num_frames 8但你在main()中可直接修改const int NUM_FRAMES 16;来测试不同内存容量的影响。3.4 避坑内存管理模块的五个血泪经验现象access_sequence.txt输入 100 个页号但程序只处理前 20 个就退出原因mem_sim.cpp中read_access_sequence()函数使用std::vectorint sequence(20)预分配大小后续push_back超出容量时未扩容。解决将sequence声明为std::vectorint sequence;去掉预分配或在读取前sequence.reserve(expected_size);现象LRU 替换总是选中同一个帧如帧 0导致其他帧永远闲置原因access_time初始化为 0所有有效页初始时间戳相同find_lru_frame()总返回第一个匹配项。解决在PageTable::load_page(int page_num)中首次加载时设ptes[page_num].access_time global_timestamp;确保时间戳唯一递增现象print_physical_memory_status()显示[0] P3但P3的frame_number却是 5明显矛盾原因ptes[i].frame_number存储的是物理帧号但输出时误用了i逻辑页号作为帧号索引。解决输出循环应遍历物理帧for (int frame 0; frame num_frames; frame)再反查哪个 PTE 的frame_number frame现象修改NUM_FRAMES为 4 后程序崩溃在ptes[page_num].frame_number frame;原因frame变量在allocate_frame()中未做边界检查当free_frames为空时仍尝试取free_frames.back()。解决在allocate_frame()开头加if (free_frames.empty()) { handle_page_fault(); return -1; }现象缺页中断计数page_fault_count比手动计算少 1原因handle_page_fault()函数中page_fault_count放在load_page()之后但load_page()内部可能因帧不足再次触发缺页。解决将page_fault_count移至handle_page_fault()开头确保每次进入该函数即计数4. 实验报告生成器从 raw output 到符合西南科大格式的 LaTeX 报告模板4.1 output.txt 的结构化解析用 Python 脚本自动提取关键数据cpp2.cpp和mem_sim.cpp的输出都是纯文本但西南科大实验报告要求表格化呈现。我写了一个parse_output.py脚本附在资源包tools/目录它能自动识别output.txt中的 Gantt 图、进程统计表、缺页日志并生成 CSV# parse_output.py 核心逻辑 import re with open(output.txt) as f: lines f.readlines() # 提取 Gantt 图匹配 [P\d:\d-\d] 模式 gantt_match re.search(r\[P\d:\d-\d\](?:\[P\d:\d-\d\])*, .join(lines)) if gantt_match: gantt_str gantt_match.group() # 解析为 [(pid, start, end), ...] segments re.findall(rP(\d):(\d)-(\d), gantt_str) gantt_data [(int(pid), int(start), int(end)) for pid, start, end in segments] # 提取进程统计匹配 PID.*\d\s\d\s\d proc_lines [l for l in lines if re.match(r^\s*\d\s\d\s\d\s\d, l)] for line in proc_lines: parts list(map(int, line.strip().split())) # parts[0]PID, parts[1]Completion, parts[2]Turnaround, parts[3]Waiting参数说明脚本默认读取output.txt输出gantt.csv三列PID, Start, End和stats.csv四列PID, Completion, Turnaround, Waiting。你可用 Excel 或 Pandas 直接绘图。4.2 LaTeX 报告模板直接填充数据避免 Word 排版灾难资源包中report_template.tex是西南科大信科院标准格式1.5 倍行距、宋体小四、图表居中。它预留了三个数据插入点\input{tables/gantt_table.tex}由gantt.csv自动生成的甘特图 LaTeX 表格用csvsimple宏包\input{tables/process_stats.tex}进程统计表含 AWT/ATT 计算公式\input{tables/memory_stats.tex}内存模块的缺页率、命中率、平均驻留时间编译命令只需一行pdflatex report_template.tex。无需安装宏包——csvsimple已包含在模板导言区。4.3 死锁场景的专项分析如何用银行家算法验证 input.txt 中的资源请求原包test_cases/deadlock_case.txt提供了一组典型的死锁输入4 进程 × 3 资源类。cpp2.cpp本身不实现银行家算法但你可以用tools/banker.py快速验证# tools/banker.py 示例验证 deadlock_case.txt def is_safe_state(available, max_need, allocation): work available.copy() finish [False] * len(max_need) safe_sequence [] while False in finish: found False for i in range(len(max_need)): if not finish[i]: # 检查该进程所有资源需求是否 work if all(max_need[i][j] - allocation[i][j] work[j] for j in range(len(work))): # 分配资源标记完成 for j in range(len(work)): work[j] allocation[i][j] finish[i] True safe_sequence.append(i) found True break if not found: return False, [] return True, safe_sequence提示deadlock_case.txt的格式是Available: 3 3 2→Max: [[7,5,3],[3,2,2],[9,0,2],[2,2,2]]→Allocation: [[0,1,0],[2,0,0],[3,0,2],[2,1,1]]。脚本会输出Safe sequence: [0, 1, 3, 2]或Unsafe state detected。4.4 避坑报告生成环节的三个隐形陷阱现象LaTeX 编译报错! Package csvsimple Error: File gantt.csv not found.原因report_template.tex中\input{tables/gantt_table.tex}的路径是相对report_template.tex的但parse_output.py默认输出到当前目录。解决运行parse_output.py前先mkdir -p tables/再让脚本输出到tables/gantt.csv现象甘特图表格中 PID 列显示为P1但 LaTeX 报告要求纯数字1原因parse_output.py从 Gantt 字符串提取时保留了P前缀。解决在segments re.findall(rP(\d):(\d)-(\d), gantt_str)中(\d)已捕获纯数字无需额外处理现象banker.py输出Safe sequence: [0, 1, 3, 2]但报告里写成P0,P1,P3,P2与实验指导书要求的P1,P2,P4,P3格式不符原因索引从 0 开始但西南科大报告习惯用 1-based PID。解决在banker.py输出前将safe_sequence中每个元素1并拼接为P str(i)5. VS Code 调试配置实战解决 cpp 头文件报红、IntelliSense 误判、多文件构建难题5.1 c_cpp_properties.json 的精准配置让 IntelliSense 识别自定义头文件cpp2.cpp和mem_sim.cpp无头文件但若你扩展功能如加scheduler.hVS Code 默认不识别#include scheduler.h。根本原因是c_cpp_properties.json中browse.path未包含当前目录。正确配置如下{ configurations: [ { name: Linux, includePath: [${workspaceFolder}/**], defines: [], compilerPath: /usr/bin/g, cStandard: c17, cppStandard: c17, intelliSenseMode: linux-gcc-x64, browse: { path: [${workspaceFolder}], limitSymbolsToIncludedHeaders: false } } ], version: 4 }关键参数browse.path必须设为${workspaceFolder}不是${workspaceFolder}/**否则 IntelliSense 会忽略子目录limitSymbolsToIncludedHeaders: false允许跨文件符号跳转。5.2 tasks.json 的多文件编译任务一键编译 cpp2 和 mem_sim原包是单文件但你很可能要拆分成scheduler.cppprocess.cppmain.cpp。tasks.json需指定所有源文件{ version: 2.0.0, tasks: [ { type: shell, label: C/C: g build active file, command: /usr/bin/g, args: [ -g, ${fileDirname}/*.cpp, // 关键通配符编译同目录所有 .cpp -o, ${fileDirname}/output ], group: build, problemMatcher: [$gcc] } ] }注意${fileDirname}/*.cpp在 Windows 上需改为${fileDirname}\\*.cpp但更稳妥的是显式列出cpp2.cpp, mem_sim.cpp。5.3 launch.json 的进程调度断点调试观察就绪队列的实时变化要在Scheduler::run_rr()中观察ready_queue内容需在while循环内设断点并启用Debug Console查看 STL 容器{ version: 0.2.0, configurations: [ { name: g - Build and debug active file, type: cppdbg, request: launch, program: ${fileDirname}/output, args: [], stopAtEntry: false, cwd: ${fileDirname}, environment: [], externalConsole: false, MIMode: gdb, setupCommands: [ { description: Enable pretty-printing for gdb, text: -enable-pretty-printing, ignoreFailures: true } ], preLaunchTask: C/C: g build active file } ] }调试技巧在ready_queue.front()断点处在 Debug Console 输入p ready_queue._M_impl._M_startGCC libstdc或p ready_queue._M_impl._M_startClang libc可查看队列底层指针。但更实用的是右键ready_queue→Debug: Add to WatchVS Code 会自动展开显示内容。5.4 避坑VS Code C 环境的四大玄学问题现象#include vector报红但编译成功原因IntelliSense 使用的compilerPath与实际编译器不一致如 VS Code 配了/usr/bin/clang但tasks.json用g。解决统一c_cpp_properties.json的compilerPath和tasks.json的command或在c_cpp_properties.json中设compilerPath: /usr/bin/g现象修改cpp2.cpp后CtrlF5重启调试但执行的仍是旧二进制原因launch.json的program路径未随构建输出更新。原包tasks.json输出到output但launch.json可能指向./a.out。解决将launch.json的program改为${fileDirname}/output与tasks.json的-o参数一致现象在mem_sim.cpp中std::vectorint physical_memory(NUM_FRAMES, -1);报错expected a type specifier原因VS Code 默认 C 标准为 C14而std::vector初始化列表语法需 C11但某些老版本 IntelliSense 误判。解决在c_cpp_properties.json中明确cppStandard: c11或c17现象Debug Console输入p ptes[0].valid_bit显示Cannot evaluate expression原因GDB 未加载调试符号或ptes是局部变量已被优化。解决编译时加-g -O0tasks.json的args中加入-O0并在launch.json的setupCommands中添加-enable-pretty-printing6. 从西南科大实验到工业级系统编程一个内存泄漏检测技巧与我的强制习惯6.1 用 valgrind 捕捉 cpp2.cpp 中的隐性内存泄漏cpp2.cpp用new Process动态创建进程对象但从未delete。这不是教学疏忽而是故意留的“坑”——让你学会用工具发现它。在 Linux 下编译后运行g -g -stdc11 cpp2.cpp -o scheduler valgrind --leak-checkfull --show-leak-kindsall ./scheduler输出会明确指出12345 40 bytes in 5 blocks are definitely lost in loss record 1 of 1 12345 at 0x4C3017F: operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so) 12345 by 0x1091A2: main (cpp2.cpp:45)行号 45 正是new Process(...)的位置。修复方法很简单在main()结尾加循环deletefor (auto* p : processes) { delete p; }但注意processes是std::vectorProcess*delete后指针变 dangling所以必须在delete后p nullptr或改用std::unique_ptrProcess。6.2 我的强制习惯每次修改调度逻辑后必跑三组验证用例西南科大test_cases/目录下有 5 个用例但我只固定用三个rr_simple.txt3 进程到达时间错开burst 时间短验证 RR 时间片切分是否精确priority_starvation.txt1 个高优先级进程priority1持续运行2 个低优先级priority10长期等待验证饥饿是否发生memory_lru.txt16 页访问序列NUM_FRAMES4理论缺页率应 ≥60%验证 LRU 是否真比 FIFO 好我写了个run_all.sh脚本自动编译、运行、比对输出#!/bin/bash for case in rr_simple priority_starvation memory_lru; do echo Testing $case cp test_cases/${case}.txt input.txt g -g -stdc11 cpp2.cpp -o scheduler ./scheduler # 检查 output.txt 是否包含 Average Waiting Time: if grep -q Average Waiting Time: output.txt; then echo ✓ $case passed else echo ✗ $case failed fi done教训去年帮学弟改 Priority 调度他只测了priority_simple.txt所有进程同时到达结果上线后遇到priority_starvation.txt场景直接卡死。从那以后我每次改调度策略都强制走一遍这三组用例——不是为了交作业而是建立对算法边界的肌肉记忆。6.3 为什么不用 Docker 封装这个实验环境有人问既然要跨平台为何不做成 Docker 镜像我的答案是这个实验的价值不在环境隔离而在亲手触摸内存地址、进程状态、页表项的每一次变更。Docker 会抽象掉g的具体版本、valgrind的输出细节、VS Code 的 IntelliSense 行为——而这些恰恰是调试时最真实的线索。西南科大的实验设计本质是让你在“可控的简陋”中理解“不可控的复杂”。当你能在裸机上用gdb看到ready_queue的_M_impl._M_start指针偏移你就比在容器里敲docker run更接近操作系统的心脏。希望帮到你。本文还有配套的精品资源点击获取