从Linux环境到软中断:一份95分操作系统实验报告全解析
简介西南交通大学操作系统实验报告完整收录胡晓鹏老师课程的六个实验涵盖Linux环境与Shell命令、C程序编译调试、/proc内核状态观测、软中断通信、进程调度及线程同步等主题适合正在学习操作系统或Linux编程的本科生对照参考。报告基于2020—2021学年软件工程专业真实作业评分达95分不仅给出每次实验的目的、步骤与总结还详细展示了cd、ls、ps、chmod等命令用法以及vi、gcc、gdb工具链操作和cpuinfo、meminfo、loadavg、interrupts等内核文件的编程观测方法。压缩包内共1个docx文档约1.48MB目录结构按实验编号清晰划分方便直接查阅或借鉴格式。该资源已有401人学习对需要完成同类实验、撰写实验报告或理解Linux内核抽象的同学具有较高参考价值可直接作为实验流程梳理、命令细节核对和报告排版范本使用。1. 一份 95 分的操作系统实验报告从读懂 Linux 到跑通软中断某高校软件工程专业的操作系统课设里有一份评分 95 的实验报告覆盖了 Linux 环境与 C 语言调试、/proc 文件系统观测、软中断通信、进程调度、线程同步、内存管理、系统调用和缺页统计八个实验。这份报告值得参考的点不在于每个实验都跑通了而在于它把「代码演示」和「机制理解」放在了一起——比如用 Ctrl\ 触发 SIGQUIT 来观察父子进程的终止顺序比如通过 /proc/cpuinfo 和 /proc/meminfo 去对应课堂上讲的内核变量。对于正在做操作系统课设或准备 OS 方向面试的人来说这套实验编排刚好覆盖了进程管理、内存管理、信号机制、系统调用四大主线。本文就按这份报告的结构拆开讲从环境准备到 /proc 观测再到软中断和进程调度最后落到内存管理、系统调用和缺页统计并给出验收和答辩的方法。如果你是照着课设要求逐项做可以直接按这个顺序推进。2. 实验环境准备Linux 目录结构、Shell 命令与 gcc/gdb 调试链操作系统实验的第一步永远是环境。报告中的实验 1 花费了大量篇幅在「熟悉 Linux 界面与目录结构」和「在终端里跑通一个 C 程序」上。这一步看起来基础实际决定了后面七个实验能不能顺利推进尤其是 fork、signal 这类依赖进程机制的程序必须在一个能稳定运行的环境里调试。2.1 Linux 目录结构先搞清楚东西放在哪报告里记录了一个很直观的观察Linux 和 Windows 的目录组织方式完全不同。Windows 用盘符C:、D:区分存储区域Linux 则是单一根目录/用正斜杠/分割路径。初次接触时容易混淆的点在于/bin存放最常用的命令ls、cp、mv/home是用户主目录的根/etc保存系统配置文件/proc是虚拟文件系统运行时的内核信息都在这里。报告特别标注了/proc下的数字目录——每个数字对应一个正在运行的进程 PID里面是进程的运行时信息。我把这些目录的用途整理成一张速查表方便对照目录路径主要用途实验中的使用场景/bin常用命令存放处ls、cp、mv的执行文件/home用户主目录存放实验代码文件/proc虚拟文件系统内核状态接口实验 1、实验 7 读取系统信息/tmp临时文件编译中间文件的临时输出/lostfound异常关机后的文件恢复区一般用不到做实验时一个典型的操作流程是用cd进入工作目录用ls确认文件存在用mv重命名文件。报告里提到一个细节想修改已编辑好的文件名在终端用mv agrc.c oslab.1.c即可如果用 vim 编辑也可以在保存时用:w new_filename直接另存为新文件。两个方法都能用前者是 Shell 层的操作后者是编辑器内的操作场景不同选择不同。2.2 Shell 命令与管道重定向不只是敲命令报告在实验 1 中列出的命令包括cd切换目录、ls列出文件、ps查看进程状态、chmod修改权限以及 redirect重定向和 pipe管道机制。这里的核心在于Shell 不只是逐条执行命令还能通过重定向把输出写入文件通过管道把前一个命令的输出作为后一个命令的输入。比如ps -ef | grep test就是查看进程列表中是否有 test 程序的经典组合ls -l filelist.txt则把目录列表保存到文件里。这两个机制在后续实验中频繁用到——软中断实验里需要观察进程 PID 和父子关系ps和grep的组合能快速确认进程是否被正确创建和终止。2.3 gcc 编译与 gdb 调试把 C 程序跑起来报告中的实验流程是用 vim 写代码按i进入编辑模式写完按ESC返回一般模式再用:wq保存退出最后用gcc编译。这里有个常见错误初次使用 vim 的人经常不知道如何进入编辑模式或者按了ESC后直接输入字符导致命令冲突。报告记录的步骤把这条路走通了进入 vim 后按i、o、a、r中的任一键都能进入编辑模式ESC返回后:开头的是命令模式:wq是保存并退出。编译阶段gcc是最直接的编译器gcc -o test test.c生成可执行文件gcc -g test.c -o test带调试信息生成。要比较 gcc、gdb、codeblocks 的区别核心在于 debug 能力gdb 支持设置断点、单步执行、查看变量值codeblocks 是图形化 IDE把编译和调试集成了但对远程环境如虚拟机中的 Linux不友好。报告中提到一个血泪经验在 Windows 的 IDE 里写好带fork()的代码复制到 Linux 后报错——因为 Windows IDE 默认的头文件和库不含 POSIX 的fork()。从那以后我的习惯是涉及进程、信号、文件锁的代码直接在 Linux 环境里写不再跨环境搬运。注意gcc编译时建议加-Wall参数查看全部警告。fork 相关的代码如果出现隐式声明警告通常是因为缺少#include unistd.h这是最容易被忽略的问题。2.4 网络配置的三种模式与踩坑记录实验报告里记录了一次网络故障排查虚拟机装好后配置没问题但网络连接一直失败。排查后定位到是虚拟机的网络模式选错了并借此把三种模式学明白了桥接模式是让虚拟机像局域网里的一台独立主机可以互访也可以上网但 IP 容易冲突NAT地址转换模式是虚拟机通过母机代理访问外网外部网络访问不到虚拟机主机模式host-only则是虚拟机和宿主机形成独立网络虚拟机无法访问互联网。最终选择 NAT 模式既避免 IP 冲突又能上网。这里给初学者一个建议操作系统实验大多不需要外部网络但需要宿主机与虚拟机之间传输代码文件。用 NAT 模式配合xftp或scp是最省心的方案如果只有一台机器做实验host-only 也够用只是不能在线查资料。如果遇到网络不通先 ping 网关再确认虚拟网卡是否启用最后检查防火墙——三个步骤能排除九成问题。3. /proc 文件系统观测内核变量的黑匣子怎么打开实验 1 和实验 7 把重点放在了/proc文件系统上。/proc是 Linux 内核向用户态暴露状态信息的虚拟文件系统读取它以获取 CPU 信息、内存状态、系统负载、中断统计等数据。它之所以重要是因为操作系统课上学到的很多概念如进程状态、内存分配、系统节拍都以文件的形式反映在这里且可以随时查阅。3.1 /proc/cpuinfo、version、stat、uptime 的观测方法报告中明确给出了用程序观测的四项内容/proc/cpuinfo提供 CPU 型号、核数、频率等信息/proc/version显示内核版本号例如 Linux 3.10.0-1160包含内核编译信息/proc/stat保存系统启动以来的 CPU 统计和进程切换数据/proc/uptime返回系统开机时长。观测这些文件最简单的方式是cat /proc/cpuinfo但课程设计要求「编程实现观测」这就涉及 C 语言的文件读取操作核心代码如下#include stdio.h #include string.h int main() { FILE *fp; char line[256]; fp fopen(/proc/version, r); if (fp NULL) { perror(open /proc/version failed); return 1; } while (fgets(line, sizeof(line), fp) ! NULL) { printf(%s, line); } fclose(fp); return 0; }这段代码本质上就是打开文件、逐行读取、打印。但设计要点在于/proc下的文件是内核动态生成的虚拟文件读取它不需要特殊权限部分需要 root但打开失败时要用perror输出错误原因。如果报No such file or directory说明当前系统的内核版本不包含该节点例如/proc/filesystems在部分精简内核中不存在——此时需要检查内核配置而不是改代码。将fopen的文件路径换成/proc/cpuinfo、/proc/stat、/proc/uptime即可观测其余项目。3.2 meminfo、loadavg、interrupts、filesystem 的编程观测报告要求编程实现对/proc/meminfo、/proc/loadavg、/proc/interrupts、/proc/filesystems的观测。这四个文件的含义分别是meminfo描述系统内存总量、空闲量、缓存量对应课程中的内存管理章节loadavg显示 1 分钟、5 分钟、15 分钟的平均负载interrupts记录各中断号的发生次数对应中断机制filesystems列出当前内核支持的文件系统类型。通常的做法是写一个通用的读取函数传入路径参数即可如下所示void show_file(const char *path) { FILE *fp fopen(path, r); char line[256]; if (!fp) { perror(path); return; } while (fgets(line, sizeof(line), fp) ! NULL) { printf(%s, line); } fclose(fp); } int main() { show_file(/proc/meminfo); show_file(/proc/loadavg); show_file(/proc/interrupts); show_file(/proc/filesystems); return 0; }这里有一个容易忽略的点/proc文件是伪文件读取时fseek、rewind等操作没有实际意义每次读取都是内核实时生成的快照。所以不要在代码里做「定位到文件末尾再读回开头」的操作直接顺序读取即可。3.3 从 jiffies 理解内核时间基准报告在实验总结中记录了一个有意思的思考为什么 Linux 的时间统计是基于「节拍」而不是「秒」答案是内核使用jiffies作为全局时间基准——内核每过一个节拍通常 1/250 秒或 1/100 秒jiffies加 1。开机时内核读取 RTC 硬件获取基准时间再换算成对应的jiffies值减去 1970-01-01 UTC 的时间差。/proc/uptime中的数值就是基于jiffies换算出来的。这个知识点在今年的面试和课程答辩中经常出现为什么 uptime 是浮点数为什么 loadavg 有三个值如果只答「记录系统启动时长」就太浅了。能说出jiffies的递增机制和系统节拍HZ的配置才算是真正理解了内核时间系统。报告中这部分属于加分项建议在答辩时主动提。4. 软中断通信实验信号机制与进程终止顺序软中断通信是这份报告中代码量最大、也最值得细读的实验。实验要求用信号如 SIGQUIT、SIGUSR1实现父子进程间的通信并观察终止顺序。核心是理解三件事fork()创建子进程后的父子关系、signal()注册信号处理函数、kill()发送信号。4.1 实验一一个父进程创建两个子进程用 Ctrl\ 终止流程是这样的父进程创建两个子进程 P1 和 P2父进程调用signal(SIGQUIT, stop)注册信号处理函数然后进入waiting()忙等待用户在终端按Ctrl\发送 SIGQUIT父进程收到信号后执行stop()将wait_mark置 0退出等待随后向 P1 发信号 16、向 P2 发信号 17两个子进程收到信号后打印结束信息并退出父进程最后wait(0)回收子进程并退出。核心代码节选void waiting() { while (wait_mark ! 0); } void stop() { wait_mark 0; } int main() { int p1, p2; while ((p1 fork()) -1); // 创建第一个子进程 sleep(1); if (p1 0) { // 父进程分支 printf(parent process is %d \n, getpid()); while ((p2 fork()) -1); // 创建第二个子进程 if (p2 0) { wait_mark 1; signal(SIGQUIT, stop); waiting(); kill(p1, 16); // 向子进程 P1 发信号 16 kill(p2, 17); // 向子进程 P2 发信号 17 wait(0); wait(0); printf(\n parent process %d is killed!\n, getpid()); exit(0); } else { // 子进程 P2 分支 printf(child P2 process %d is created by %d \n, getpid(), getppid()); signal(SIGQUIT, SIG_IGN); wait_mark 1; signal(17, stop); waiting(); lockf(1, 1, 0); printf(\n child P2 process %d is killed by parent %d , getpid(), getppid()); lockf(1, 0, 0); } } else { // 子进程 P1 分支 printf(child P1 process %d is created by %d \n, getpid(), getppid()); signal(SIGQUIT, SIG_IGN); wait_mark 1; signal(16, stop); waiting(); lockf(1, 1, 0); printf(\n child P1 process %d is killed by parent %d , getpid(), getppid()); lockf(1, 0, 0); } return 0; }代码的设计说明fork()返回 0 表示当前是子进程返回大于 0 的值是子进程 PID对父进程而言。父进程先通过fork()创建 P1sleep(1)确保 P1 完成初始化再创建 P2。为什么 P1 和 P2 要用不同的信号编号16 和 17因为这样每个子进程只响应属于自己的信号避免歧义这对应了「软中断信号用于进程间通信」的实验目的。lockf(1, 1, 0)的作用是加锁标准输出防止两个子进程同时往终端打印时输出交错这在并发编程里是一个典型问题——不加锁printf 的输出可能互相穿插。4.2 实验二父、子、孙三级进程的依次终止实验二不同于实验一的「一父两子」结构而是父进程 → 子进程 → 孙进程的链式结构。流程是父进程创建子进程 P1P1 再创建孙进程 P2按 Ctrl\ 后父进程向 P1 发信号P1 向 P2 发信号依次终止孙、子、父。进程树形结构中的终止顺序是先杀最深的再杀中间的最后杀根部的。int main() { int p1, p2; while ((p1 fork()) -1); if (p1 0) { // 父进程 wait_mark 1; signal(SIGQUIT, stop); waiting(); kill(p1, 16); wait(0); printf(\n parent process is killed \n); exit(0); } else { // 子进程 while ((p1 fork()) -1); // 创建孙进程 if (p1 0) { // 子进程分支 printf(child P1 process %d is created by the parent %d \n, getpid(), getppid()); signal(SIGQUIT, SIG_IGN); wait_mark 1; signal(16, stop); waiting(); kill(p1, 16); wait(0); printf(\n child P1 process %d is killed by parent %d \n, getpid(), getppid()); exit(0); } else if (p1 0) { // 孙进程分支 printf(grandson process %d is created by the parent %d \n, getpid(), getppid()); signal(SIGQUIT, SIG_IGN); wait_mark 1; signal(16, stop); waiting(); printf(\n grandson process %d is killed by parent %d \n, getpid(), getppid()); exit(0); } } return 0; }这段代码的逻辑层次很清晰但要注意一个隐蔽的问题子进程和孙进程都注册了信号 16 的处理函数这意味着如果父进程误发了两次信号 16孙进程和子进程可能同时退出破坏层级顺序。所以实验二的运行结果中「先孙再子」的顺序依赖代码执行路径而不是信号优先级——父进程是在wait(0)等子进程结束后才 print 自己的结束信息子进程是在wait(0)等孙进程结束后才 print 自己的结束信息这是一个隐式的顺序控制。4.3 避坑指南软中断实验中容易翻车的四个问题1. 现象fork()在编译时报错提示隐式声明。原因源文件缺少#include unistd.h部分旧代码还依赖#include sys/types.h。解决在文件头部补全头文件不要省略unistd.h如果用了wait()还要加#include sys/wait.h。2. 现象程序运行后按 Ctrl\ 没有反应。原因SIGQUIT的信号处理函数没有正确注册或者进程处于后台运行状态nohup 方式启动。解决确认用signal(SIGQUIT, stop)注册了处理函数而不是SIG_IGN确认程序在前台运行Ctrl\只对前台进程的终端有效。3. 现象父子进程的 printf 输出交叉混乱。原因多个进程同时抢占标准输出没有互斥。解决用lockf(1, 1, 0)加锁标准输出打印完再lockf(1, 0, 0)解锁。这个方法比printf前的sleep更可靠。4. 现象父进程 wait(0) 返回后子进程仍未退出。原因kill(p1, 16)发送信号后子进程可能还在waiting()忙等待中没有及时被调度。解决在信号处理函数里将wait_mark置 0 是不够的还要确保子进程收到信号后能退出waiting()循环——这里的关键是signal(16, stop)注册的处理函数确实被执行。可以用sleep(1)或waitpid()替代忙等待使时序更稳定。5. 进程调度、线程同步、内存管理与系统调用从模拟到真实实现实验 3 到实验 6 的内容覆盖了操作系统的几个核心主题进程调度策略、线程同步、内存管理和系统调用。报告中记录了 FCFS 与 RR 调度策略的验证、线程同步实验、内存管理的代码与结果解读、以及系统调用的具体实现步骤。5.1 进程调度实验FCFS 与 RR 的工程验证实验 3 要求创建工程并验证 FCFS先来先服务和 RR时间片轮转策略。FCFS 的核心指标是等待时间先到达的进程先执行短作业可能排队很久RR 的核心是时间片每个进程运行一个时间片后轮转到下一个。设计实验时需要构造一组进程分别记录到达时间、服务时间、完成时间和周转时间并用表格汇总。建议的验证方式用结构体数组保存进程控制块模拟调度过程。typedef struct { int pid; // 进程号 int arrive_time; // 到达时间 int service_time; // 服务时间 int finish_time; // 完成时间 int remain_time; // 剩余时间RR 用 } pcb;FCFS 的实现就是遍历进程数组按到达时间排序后依次执行RR 的实现需要维护一个就绪队列每次执行一个时间片如 2 个单位时间片结束后判断剩余时间是否为 0。报告中的实验结论是FCFS 的平均周转时间通常高于 RR但 RR 的响应时间更短如果进程的服务时间差异很大RR 的时间片要选得合理太小则上下文切换开销大太大则退化为 FCFS。答辩时把这个权衡讲清楚比背概念有用得多。5.2 线程同步实验锁、信号量与条件变量实验 4 的线程同步实验报告没有详细列出代码但通常的做法是经典的「生产者-消费者」或「读者-写者」模型。用pthread_mutex_t保护共享资源用pthread_cond_t实现条件等待#include pthread.h pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond PTHREAD_COND_INITIALIZER; int buffer[10]; int count 0; void *producer(void *arg) { pthread_mutex_lock(mutex); while (count 10) { pthread_cond_wait(cond, mutex); } buffer[count] 1; pthread_cond_signal(cond); pthread_mutex_unlock(mutex); return NULL; }这个代码的两个关键点pthread_cond_wait必须放在 while 循环里而不是 if 里——因为可能存在虚假唤醒pthread_cond_signal和pthread_mutex_unlock的顺序会影响唤醒后线程的调度效率。如果忘记解锁或者忘记加锁程序可能死锁或数据竞争这类问题用valgrind --toolhelgrind可以检测但最好的办法是写代码时就把「加锁-操作-解锁」的边界画清楚。5.3 内存管理实验页面置换与缺页统计实验 5 的内容是内存管理重点是页面置换算法。报告给出了实验结果展示和结果解读但没有把代码全部贴出来。常见的设计是模拟 FIFO、LRU、OPT 三种算法统计各自的缺页次数。报告中实验 7 的「系统缺页次数统计」实际就是在真实系统中观测缺页行为通过读取/proc/vmstat中的pgfault字段来统计缺页次数或者通过ps -o minflt,majflt查看某个进程的缺页计数。一个实际可用的思路在 C 程序中分配一大块内存并循环访问在访问前后分别读取/proc/vmstat的缺页计数值差值就是触发的新缺页数#include stdio.h #include stdlib.h #include string.h int main() { unsigned long before, after; FILE *fp; int size 100 * 1024 * 1024; // 100MB char *buf malloc(size); fp fopen(/proc/vmstat, r); // 读取 pgfault 字段存入 before fclose(fp); memset(buf, 1, size); // 触发大量缺页 fp fopen(/proc/vmstat, r); // 读取 pgfault 字段存入 after fclose(fp); printf(page faults: %lu\n, after - before); free(buf); return 0; }这里的设计说明memset一个 100MB 的缓冲区如果系统内存紧张会产生大量主缺页major fault和次缺页minor fault。实际操作中建议把before和after的读取封装成函数避免代码重复。5.4 系统调用实验从用户态到内核态的通道实验 6 的系统调用实验报告中的核心是编写用户态程序调用syscall或直接调用open、read、write等 API运行后观察程序的系统调用轨迹。通常的做法是用strace追踪strace -c ./test_program会统计每个系统调用的次数、耗时和错误数。这在实验报告中是加分项因为能直观展示read、write、mmap、futex这些系统调用的真实行为。如果课程要求新增一个系统调用部分高校的实验内容是给内核添加一个系统调用通常的做法是修改系统调用号表、添加内核函数实现、重新编译内核。这个操作复杂且危险可能导致系统起不来建议在虚拟机里做并做好快照。报告中的实验 6 没有走这么深停留在用户态调用层面但如果你能在答辩中说出strace的结果含义就已经超过了大多数同学。6. 报告验收与答辩如何把 95 分实验讲成自己的东西实验报告的验收通常分两部分文档审查和现场答辩。文档审查看的是格式规范、实验步骤完整性、实验结果是否有截图、代码是否可复现现场答辩看的是随机抽查的某个实验的核心机制。根据这份报告的经验我把验收作答的要点整理成一张核对表检查项合格标准常见扣分点实验目的明确列出本次实验要理解什么概念从网上抄一段「了解 Linux」就算完实验步骤能复现命令和代码对应只有代码没有运行步骤实验结果截图或输出且有解读贴了图但没解释为什么是这个输出实验总结记录遇到的问题和解决办法直接跳过或抄教材原文答辩时最容易翻车的两个问题一是「这个程序如果去掉 sleep(1) 会怎样」二是「为什么用信号 16 而不是 SIGUSR1」。前者考察对并发时序的理解——去掉sleep(1)后父进程可能在子进程完成初始化前就fork()第二个子进程导致进程关系错乱后者考察对 Linux 信号编号的掌握——SIGUSR1 在部分架构上是信号 10SIGUSR2 是信号 12但代码中选择 16 和 17 属于自定义信号需要确保内核支持。这个问题的回答思路是信号编号对应关系依赖平台课程实验中固定使用 16 和 17 是为了简单明确但实际工程中建议使用SIGUSR1并配合sigaction()注册这样可读性和可移植性都更好。此外答辩时有一个实用技巧提前准备 3 个「为什么」。比如软中断实验的代码里为什么要用wait_mark忙等待而不直接pause()因为忙等待不需要额外引入信号逻辑直观但 CPU 空转如果换成pause()可以让出 CPU但需要处理「信号到达时进程处于可中断睡眠」的状态转换。你能主动说出这个权衡说明不是只抄了代码而是真正理解了设计取舍。提示如果被问到「实验结果和预期不一致怎么办」不要慌张。先把实际输出贴出来指出哪一步和预期不同然后给出排查方向——多数情况是wait()回收顺序问题或信号编号冲突。从那以后我每次做完实验都会强制自己走一遍「去掉一个关键代码再跑一次」的流程。比如软中断实验去掉lockf观察输出是否错乱调度实验把时间片改成 1观察周转时间变化。这样做的好处是任何「为什么这样写」的问题都能用自己的实验结果来回答而不是背答案。希望帮到你。本文还有配套的精品资源点击获取