
简介西南交通大学操作系统实验报告是一份获得95分A的高分课程成果面向操作系统课程学习者、Linux初学者及需要撰写实验报告的高校学生。报告基于2020-2021学年软件工程专业教学要求完整记录多个核心实验熟悉Linux目录结构与Shell常用命令cd、ls、ps、chmod及重定向、管道机制使用vi/gedit与gcc/gdb完成C程序编写、编译与调试通过/proc虚拟文件系统观测cpuinfo、version、stat、uptime、meminfo、loadavg、interrupts等系统状态信息并涉及软中断通信、进程调度FCFS与RR策略和线程同步实验每项均含实验目的、步骤与总结方便对照实践。资源包为docx单文件约1.48MB排版清晰可直接阅读。目前已有401人学习浏览适合需要系统梳理Linux实验要点、提升内核观测与C调试能力的读者参考。1. 为什么操作系统实验要当成一套「Linux 全链路打通」来做操作系统实验能拿到 95 分往往不是最后一个代码能跑而是整条链路能不能被讲清楚。我见过不少同学在截止前几天把七个实验的 C 程序一次性凑齐代码也确实出结果但验收时被追问一个底层原理就直接卡壳。这套来自某高校软件工程专业的操作系统实验方案把 Linux 环境搭建、Shell 命令、gcc/gdb 调试、软中断通信、进程调度、线程同步、内存管理、系统调用与缺页统计七个实验串成了一条可复现的实践链路。对正在做操作系统课设或毕设的学生来说它可以当作搭建自己方案的底稿对想快速回顾 Linux 进程管理与 /proc 机制的从业者也是一份合适的参考材料。2. Linux 环境搭建与调试工作流目录结构、Shell 命令与 gcc/gdb2.1 目录结构与 Shell 命令先弄清 Linux 的“家底”第一次进 Linux 终端最直观的感受是所有东西都挂在一个根目录/下面路径用正斜杠分隔这和 Windows 的盘符加反斜杠完全不同。很多同学实验一卡住就是因为连目录结构都摸不清源码随手乱放最后编译时怎么也找不到路径。这套实验的第一步就是先把目录体系记下来。实验报告里重点列出的目录足够覆盖全部实验场景/bin存放最常用命令/home是用户主目录/media挂载外接设备/opt放额外安装的软件/lib是 C 编译器的库目录/tmp放临时文件。而/proc是进程信息的虚拟文件系统观察系统状态时每天都会用到但千万不要往里面写入内容。实验源码统一放在/home下的用户目录编译产生的临时文件放到/tmp系统目录一律只读这三条习惯能避免大半环境问题。Shell 命令方面重点掌握cd、ls、ps、chmod以及重定向 redirect 与管道 pipe 的组合用法。单个命令单独用理解不了系统全貌组合起来才有效率# 编译并保留错误日志避免刷屏导致第一条报错被淹没 gcc -o myprog myprog.c 2 build.log # 只查看编译日志的最后 30 行快速定位第一个错误位置 tail -30 build.log # 过滤进程输出确认 fork 出来的子进程是否还在运行 ps -ef | grep myprog管道把ps和grep串起来是实验期间排查进程的固定手段。重定向时2只捕获标准错误输出这样 stdout 和 stderr 分离调试阶段定位问题会快很多。2.2 用 Vim gcc gdb 建立调试闭环第一次在没有图形 IDE 的 Linux 下写 C 程序确实不适应但 vim 作为编辑器、gcc 作为编译器、gdb 作为调试器三者组合完全够用而且比 IDE 更能看清编译和链接的细节。vim 只需要记住几个操作按i进入编辑模式写代码按ESC退回一般模式输入:wq保存退出。需要另存文件时:w 新文件名就能搞定不用退回 Shell 再用mv。gcc 编译这步最容易掉链子。很多同学不带-g就编译真到 gdb 调试时发现没有符号信息只能回头重新编译。建议每次编译都带上完整参数gcc -Wall -g -o myprog myprog.c-Wall让所有可能的警告都显现-g生成调试信息。gdb 打开可执行文件后先用break main在入口下断点run启动next单步print查看变量值。对于 fork 多进程的调试gdb 还需要根据需求设置 follow-fork-mode否则只能跟踪到父进程或子进程其中一方。软中断通信实验里的wait_mark全局变量用print一下就能确认信号处理函数是否被正确调用比打日志定位快得多。2.3 环境问题排查网络连接与文件传输的踩坑记录实验环境搭建中网络问题排在第一位。常见现象是虚拟机配置向导全部走完系统正常启动但终端里 ping 外网持续失败。这个现象多半是虚拟机的网络模式选错了。桥接模式下虚拟机和宿主机处于同一网段互相能访问但局域网内设备一多就容易 IP 冲突NAT 模式允许虚拟机访问外网外部机器却无法反向访问虚拟机相当于虚拟机通过宿主机代理上网主机模式则将虚拟机和宿主机网络完全隔离只能本机连通访问不了外网。实验环境采用 NAT 模式最合适既能安装软件包和拉取依赖又天然避免 IP 冲突。文件传输也存在一个隐藏的坑。如果代码先在 Windows 编辑器里写好再通过 xftp 传到 Linux需要特别注意代码中是否出现了fork()、signal()这类 POSIX 接口。Windows 本地运行时这些函数没有对应实现IDE 会直接报错但 Linux 下不一定有问题。解决方式是不要依赖 Windows 环境做编译验证统一传到 Linux 内用 gcc 重新编译一次所有报错以 Linux 下的结果为准。3. /proc 文件系统观测六个内核状态接口的读取与编程实现3.1 先理解 /proc 是什么/proc 不是硬盘上真实存在的文件而是内核运行期间动态生成的虚拟文件系统。在 /proc 目录下能看到两类条目一类是纯数字的子目录每个数字对应当前正在运行的一个进程号目录内保存该进程的运行信息另一类是系统级的文件如 cpuinfo、meminfo、version、stat、uptime 等分别对应 CPU 信息、内存信息、内核版本、系统状态与运行时长。实验的目的在于通过读取这些文件理解 Linux 内核向用户态暴露状态的方式。以下六个文件是课设中重点观测对象/proc/cpuinfo读取 CPU 型号与特性/proc/version读取内核版本号/proc/stat读取进程状态与 CPU 使用统计/proc/uptime读取系统运行时长/proc/meminfo读取内存与交换分区使用量/proc/loadavg读取系统平均负载。/proc/interrupts则用于查看中断使用情况。注意读取 /proc 不需要 root 权限但写入虚拟文件会直接影响内核行为。实验一律只做读取不做写入。3.2 读取 cpuinfo、version、stat、uptime最直接的获取方式是使用 cat 命令查看cat /proc/cpuinfo cat /proc/version cat /proc/stat cat /proc/uptime/proc/version 输出中的内核版本号遵循一套规律——主版本号的第二位为偶数代表稳定版本奇数代表开发版本。这个细节常被导师在验收时追问属于必须掌握的常识。例如 3.10.0-1160 这类版本号写入报告时只需要说明版本解析规则不需要标新立异。/proc/uptime 会输出两个数字第一个是系统总运行秒数第二个是系统空闲秒数。两者之差除以总时间就是系统忙时比例报告里用一行计算就能解读负载状态。实验要求对比 uptime 和 cpuinfo 的输出本质上是让你从 CPU 属性和实际运行时长两个维度交叉验证系统状态。3.3 编程实现对 meminfo、loadavg、interrupts 的观测用 cat 看文件只是基础编程读取才是实验的核心。我习惯把读取逻辑单独封装成函数通过命令行参数决定查哪一项这样三个观测点共用一套框架#include stdio.h #include stdlib.h #include string.h void read_meminfo() { FILE *fp fopen(/proc/meminfo, r); char line[256]; if (fp NULL) { perror(open meminfo failed); exit(1); } while (fgets(line, sizeof(line), fp) ! NULL) { // 只输出前四行关键字段避免整文件刷屏 if (strncmp(line, MemTotal, 8) 0 || strncmp(line, MemFree, 7) 0 || strncmp(line, Buffers, 7) 0 || strncmp(line, Cached, 6) 0) { printf(%s, line); } } fclose(fp); } void read_loadavg() { FILE *fp fopen(/proc/loadavg, r); double run_1, run_5, run_15; int running, total, last_pid; if (fp NULL) { perror(open loadavg failed); exit(1); } // 依次解析三个平均负载字段、运行进程数与总进程数、最近进程号 fscanf(fp, %lf %lf %lf %d/%d %d, run_1, run_5, run_15, running, total, last_pid); printf(loadavg: %.2f %.2f %.2f, running %d/%d, last_pid %d\n, run_1, run_5, run_15, running, total, last_pid); fclose(fp); } int main(int argc, char **argv) { if (argc 2) { fprintf(stderr, Usage: %s [meminfo|loadavg|interrupts]\n, argv[0]); return 1; } if (strcmp(argv[1], meminfo) 0) { read_meminfo(); } else if (strcmp(argv[1], loadavg) 0) { read_loadavg(); } else { fprintf(stderr, unsupported argument\n); return 1; } return 0; }loadavg 的输出里三个小数分别表示 1 分钟、5 分钟和 15 分钟内的平均负载running/total 是当前运行进程数与总进程数last_pid 是最近创建的进程号。平均负载高于 CPU 核心数时说明系统已经过载这是一项可以结合硬件配置来叙述的分析点。interrupts 的读取逻辑与 meminfo 完全一致用 fopen 打开文件后按固定格式逐行解析即可不需要额外引入复杂的数据结构。4. 软中断通信实验从 fork 到信号的进程控制全流程4.1 信号机制与实验目标软中断通信是操作系统课程里最能说明异步机制的一组实验。目标分为两层第一层理解信号明白信号是操作系统与进程之间的一种软中断手段第二层用自定义信号在父子进程之间实现按序终止。整个实验通过fork()创建进程树通过signal()注册信号处理函数通过kill()向指定进程发送信号。实验有两个递进场景。场景一父进程创建两个子进程终端输入 Ctrl\ 后父进程收到 SIGQUIT再依次终止两个子进程并退出。场景二父进程创建子进程子进程再创建孙进程Ctrl\ 后信号从父传到子、从子传到孙终止顺序为孙、子、父。两个场景共享同一套基本代码框架区别只在于进程树的形状。完成场景一再改场景二工作量会小很多。4.2 场景一一个父进程创建两个子进程核心代码可以浓缩为以下结构省略了 lockf 在终端输出时的加锁保护#include stdio.h #include stdlib.h #include signal.h #include unistd.h int wait_mark 1; // 信号处理函数将等待标志置 0waiting() 因此退出循环 void stop() { wait_mark 0; } void waiting() { while (wait_mark ! 0) ; } int main() { int p1, p2; // 创建 P1fork 返回 -1 时持续重试 while ((p1 fork()) -1) ; sleep(1); if (p1 0) { // 父进程分支继续创建 P2 while ((p2 fork()) -1) ; if (p2 0) { wait_mark 1; signal(SIGQUIT, stop); waiting(); // 向两个子进程发送自定义信号通知它们退出 kill(p1, 16); kill(p2, 17); wait(0); wait(0); printf(parent process %d is killed!\n, getpid()); exit(0); } else { // P2 分支忽略 SIGQUIT只等待 17 号信号 wait_mark 1; signal(SIGQUIT, SIG_IGN); signal(17, stop); waiting(); printf(child P2 process %d is killed by parent %d\n, getpid(), getppid()); exit(0); } } else { // P1 分支忽略 SIGQUIT只等待 16 号信号 wait_mark 1; signal(SIGQUIT, SIG_IGN); signal(16, stop); waiting(); printf(child P1 process %d is killed by parent %d\n, getpid(), getppid()); exit(0); } }wait_mark 是信号响应开关的关键。初始为 1waiting() 做空转stop() 被信号处理器调用后把 wait_mark 置 0waiting() 退出进程继续执行。两个子进程都用SIG_IGN忽略 SIGQUIT是为了保证 Ctrl\ 只触发父进程的信号处理否则终端会把信号发给前台进程组中的所有进程逐级终止的观察效果就不存在了。kill(p1, 16)这里的 16 是自定义信号编号并非固定标准。不同 Linux 发行版上某些编号可能被占用如果实验环境特殊最好先通过kill -l查看本机可用信号编号避免信号被系统拦截后不起作用。4.3 场景二祖孙三代进程的终止顺序场景二在代码结构上改成链式传递父进程创建子进程子进程再创建孙进程。父进程收到 SIGQUIT 后向子进程发送 16 号信号子进程收到后先向孙进程发送 16 号信号等孙进程退出后再退出最后父进程退出。int main() { int p1; // 第一次 fork创建子进程 while ((p1 fork()) -1) ; if (p1 0) { wait_mark 1; signal(SIGQUIT, stop); waiting(); kill(p1, 16); wait(0); printf(parent process is killed\n); exit(0); } else { // 子进程继续 fork 创建孙进程 while ((p1 fork()) -1) ; if (p1 0) { wait_mark 1; signal(SIGQUIT, SIG_IGN); signal(16, stop); waiting(); kill(p1, 16); wait(0); printf(child P1 process %d is killed by parent %d\n, getpid(), getppid()); exit(0); } else { // 孙进程等待信号打印后退出 wait_mark 1; signal(SIGQUIT, SIG_IGN); signal(16, stop); waiting(); printf(grandson process %d is killed by parent %d\n, getpid(), getppid()); exit(0); } } }这个场景里最容易搞错的是各分支的退出顺序。正确设计里孙进程不要调用 wait()它只需要等信号、打印退出子进程在向孙进程发完信号后调用 wait(0) 回收父进程对子进程做同样处理。如果子进程提前调用 exit() 或者父进程少写了一个 wait(0)终端输出就会缺少孙进程被终止的记录整个链路断在半截。4.4 实验结果解读与验证步骤软中断实验的验证建议按四步走第一步编译源码并启动程序第二步另开终端查看进程树第三步回到程序终端按 Ctrl\第四步对比退出顺序并把日志写入实验报告。直接跑代码很容易但报告里必须写出“子进程先退出父进程最后退出”的完整顺序。执行顺序在场景一中存在随机性。由于 fork 之后两个子进程的调度顺序不完全确定sleep(1) 只能稳定父进程先与子进程打印信息P1、P2 谁先被终止仍要看当时的调度结果。实验报告可以如实记录这个现象并说明原因反而比刻意调整时序更有说服力。5. 进程调度、线程同步、内存管理与缺页统计联合落地与常见问题排查5.1 进程调度实验FCFS 与 RR 策略的验证进程调度实验要求在创建的工程中分别验证 FCFS先来先服务和 RR时间片轮转两种策略。FCFS 的设计逻辑很直白维护一个按到达时间排序的队列进程按到达顺序依次占用 CPU 直到结束RR 则在就绪队列中循环分配 CPU每个进程运行一个时间片时间片结束后回到队尾继续排队。模拟调度时通常会定义一个进程控制块结构体里面放入 pid、到达时间、服务时间和剩余时间typedef struct { int pid; int arrival; int burst; int remaining; } proc_t;FCFS 的验证重点是平均周转时间进程从进入到结束的总时间之和再除以进程数。RR 的验证重点则是时间片大小对平均周转时间的影响——时间片过大会接近 FCFS时间片过小会增加上下文切换开销课堂演示中通常给出一个适中值。报告里应当把两种策略的运行结果放在一起做数值对比而不是只贴两张截图。5.2 线程同步实验锁与信号量的作用线程同步实验通常用共享计数器或共享缓冲区来展示数据竞争。多个线程同时执行 counter 自增由于线程切换可能发生在自增指令之间最终计数值常常小于预期。加锁之后的计数值与理论值一致这就是同步存在的意义。核心代码只需展示锁的用法pthread_mutex_t lock PTHREAD_MUTEX_INITIALIZER; int counter 0; void *worker(void *arg) { for (int i 0; i 1000; i) { pthread_mutex_lock(lock); counter 1; pthread_mutex_unlock(lock); } return NULL; }实验报告中需要写清楚一个细节互斥锁只是让临界区互斥并不会改变线程被调度的顺序。如果作业要求的同步目标是按特定顺序执行比如生产者必须先于消费者生产那就得用信号量或条件变量互斥锁不够用。答辩常问“锁和信号量有什么区别”从“互斥 vs 排序”两个角度回答就能讲透。5.3 内存管理与系统调用实验从接口到机制内存管理实验的核心是认识虚拟地址到物理地址的映射关系以及进程如何向内核申请和释放内存。常见的落地方式是调用 getpagesize() 查看本机页大小再通过读取 /proc/self/stat 或 sysinfo 获取进程内存使用情况。页大小决定了分页机制下的最小分配单位做完这组实验再去做缺页统计会顺畅得多。系统调用实验的重点是观察用户态到内核态的切换过程。以最常用的获取进程号为例正常编程用库函数 getpid()但内部实际是通过 syscall 指令陷入内核完成的#include sys/syscall.h #include unistd.h // 直接通过系统调用号调用绕过库函数封装 long pid syscall(SYS_getpid);SYS_getpid 是内核为 getpid 分配的系统调用号用户态程序把系统调用号和参数放进寄存器触发 syscall 指令后由内核完成真正的执行。实验报告写到这一层就已经从“会用函数”上升到了“理解机制”。5.4 缺页次数统计实验从统计字段到结论解读缺页次数统计实验的数据来源在 /proc/self/stat其中 minflt 表示次要缺页次数majflt 表示主要缺页次数。次要缺页是指所需页面已在内存中但不在当前进程页表中主要缺页则需要从磁盘换入页面性能开销完全不同。读取这两个字段的 C 代码可以这样写FILE *fp fopen(/proc/self/stat, r); int pid; char comm[256]; unsigned long minflt, majflt; // 跳过前面多个字段第 10 和第 11 个字段分别是 minflt 与 majflt fscanf(fp, %d %s %*s %*d %*d %*d %*d %*d %*d %*d %*d %*d %lu %lu, pid, comm, minflt, majflt); printf(PID %d, minflt%lu, majflt%lu\n, pid, minflt, majflt); fclose(fp);%*s表示跳过字符串字段%*d跳过整型字段%lu读取无符号长整型。这段代码先统计一次当前进程的缺页数再访问一个较大数组触发缺页然后再读取一次并计算差值就是实验要求的缺页统计过程。结果解读时要注意缺页本身是正常的内存管理机制只有总是出现 majflt才说明物理内存真的不够用了。5.5 最容易翻车的三个点现象一代码扔到 Linux 下gcc 报出“未定义引用”错误。原因代码中调用了 fork()、signal() 等 POSIX 接口但编译环境缺失对应的头文件路径或链接库。解决编译命令中显式携带 -Wall并确认头文件已经 include优先使用 gcc 在 Linux 内编译验证不要只在 Windows IDE 上看到编译通过就认为没有问题。现象二按 Ctrl\ 后所有进程同时退出完全看不到逐级终止。原因子进程没有忽略 SIGQUIT终端把信号发送给前台进程组中的所有进程父进程和所有子进程同时响应。解决在每个子进程分支中调用 signal(SIGQUIT, SIG_IGN)保证只有父进程处理该信号再通过 kill() 分发自定义信号。现象三读取 /proc/self/stat 时数值解析错乱minflt 和 majflt 得到巨大异常值。原因/proc 文件字段顺序复杂前面的进程名可能因为带空格或括号而影响格式化解析。解决先用 cat 命令查看该文件的真实格式再对照字段顺序写 fscanf必要时以 %*s 显式跳过不受控字段。6. 验收与答辩把实验方案讲成一套能落地的故事答辩能不能过关取决于三个准备动作结构梳理、代码重点、结果解读。整个验收环节的核心不是“代码能不能跑”而是“每个实验的设计意图能不能说清楚”。结构上每份实验报告都按照目的、内容、步骤、结果、总结五段来组织。最容易跳过的“实验结果解读”恰好是评分时拉开差距的地方。不要只贴运行截图要有对比和原因分析例如软中断实验中输出顺序存在随机性根本原因是 fork 后的调度时序不确定FCFS 和 RR 的平均周转时间差异来源于是否抢占。代码重点上每段代码准备两个讲解点一个关键逻辑一个易错点。以软中断通信实验为例先说 wait_mark 是什么再说如果子进程没有忽略 SIGQUIT 程序会怎么翻车。能说清楚这两点基本就能证明代码不是从别处直接复制来的。答辩问答环节可以提前准备一张自查表问题/proc 是真实文件系统吗回答不是它是内核动态生成的虚拟文件系统用于暴露系统状态。问题FCFS 与 RR 的核心区别回答FCFS 没有抢占调度到结束RR 用时间片轮转可以抢占。问题缺页统计中 minflt 和 majflt 的含义回答minflt 是页面未映射但已在物理内存majflt 需要磁盘 I/O 补充页面。从那以后我每次做实验报告都强制自己走一遍“结构、代码、解读”的顺序宁可少写一点功能代码也要把每条关键步骤的原因写清楚。希望这套方案也能帮到你。本文还有配套的精品资源点击获取