从Linux环境到软中断:一份95分操作系统实验报告全解析

发布时间:2026/10/24 9:34:09
从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临时文件编译中间文件的临时输出
/lost+found异常关机后的文件恢复区一般用不到

做实验时一个典型的操作流程是:用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()创建 P1,sleep(1)确保 P1 完成初始化,再创建 P2。为什么 P1 和 P2 要用不同的信号编号(16 和 17)?因为这样每个子进程只响应属于自己的信号,避免歧义,这对应了「软中断信号用于进程间通信」的实验目的。lockf(1, 1, 0)的作用是加锁标准输出,防止两个子进程同时往终端打印时输出交错,这在并发编程里是一个典型问题——不加锁,printf 的输出可能互相穿插。

4.2 实验二:父、子、孙三级进程的依次终止

实验二不同于实验一的「一父两子」结构,而是父进程 → 子进程 → 孙进程的链式结构。流程是:父进程创建子进程 P1,P1 再创建孙进程 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 --tool=helgrind可以检测,但最好的办法是写代码时就把「加锁-操作-解锁」的边界画清楚。

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 在部分架构上是信号 10,SIGUSR2 是信号 12,但代码中选择 16 和 17 属于自定义信号,需要确保内核支持。这个问题的回答思路是:信号编号对应关系依赖平台,课程实验中固定使用 16 和 17 是为了简单明确,但实际工程中建议使用SIGUSR1并配合sigaction()注册,这样可读性和可移植性都更好。

此外,答辩时有一个实用技巧:提前准备 3 个「为什么」。比如软中断实验的代码里为什么要用wait_mark忙等待而不直接pause()?因为忙等待不需要额外引入信号,逻辑直观,但 CPU 空转;如果换成pause()可以让出 CPU,但需要处理「信号到达时进程处于可中断睡眠」的状态转换。你能主动说出这个权衡,说明不是只抄了代码,而是真正理解了设计取舍。

提示:如果被问到「实验结果和预期不一致怎么办」,不要慌张。先把实际输出贴出来,指出哪一步和预期不同,然后给出排查方向——多数情况是wait()回收顺序问题或信号编号冲突。

从那以后我每次做完实验,都会强制自己走一遍「去掉一个关键代码再跑一次」的流程。比如软中断实验去掉lockf,观察输出是否错乱;调度实验把时间片改成 1,观察周转时间变化。这样做的好处是:任何「为什么这样写」的问题都能用自己的实验结果来回答,而不是背答案。希望帮到你。

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