
简介这是一份面向高校软件工程与计算机专业学生的操作系统实验报告文档来自西南交通大学课程实践获评分A95分适合正在学习Linux操作系统、需要完成实验报告或复盘课程实验的读者参考。文档以单个docx文件交付压缩包约1.48MB内容按实验组织覆盖Linux用户界面与目录结构、Shell常用命令及重定向与管道机制、C语言程序编辑编译与gdb调试、/proc文件系统下cpuinfo、meminfo、loadavg、interrupts等内核状态观测并延伸至软中断通信、进程调度与线程同步实验实验链路较完整。报告结构完整含个人声明、目录、实验目的、实验内容、步骤与总结便于对照课程要求梳理实验流程、理解内核变量观测方法、复盘进程调度策略的验证思路。目前已有404人学习适合需要完成课程实验报告、补充Linux实践记录或准备相关实验考核的读者参考也可作为理解操作系统实验组织方式的样例材料。1. 知识地图八次实验其实只讲四件事把 linux 操作系统这套实验按「内核向外暴露了什么信息」重新排列八次实验会变成四条主线命令行与/proc是读取内核信息的入口fork与signal是管理进程的入口FCFS/RR 与互斥量是调度和并发的入口内存分配、系统调用、缺页计数是资源管理的入口。四条线看起来独立却共用同一套工具——gcc编译、gdb断点、cat /proc/xxx看实时数值。这份材料适合两类人正在跟课做实验、需要一份能照着改的骨架以及期末想把零散实验整理成体系、答题时能说清「为什么这样写」的人。真正拉开分数的从来不是代码能不能跑通而是你能不能解释进程树长什么样、缺页为什么是这个量级。2. Linux 环境与 /proc命令、目录与内核状态的读取方式2.1 目录结构一棵树而不是一堆盘符Linux 只有一个根/所有分区、设备、网络文件系统都是「挂载」到某个目录上的而不是先有 C:、D: 再往里塞东西。这个差别影响后面每一次路径书写绝对路径从/起算相对路径从当前工作目录起算分隔符永远是正斜杠/反斜杠在 Shell 里是转义符。把常用目录整理成一张表比死记更能应付提问目录存放内容复习时要能答出的点/bin最常用的用户命令与/sbin的分工后者面向系统管理/home普通用户主目录登录后的默认工作目录就是~/lostfound非正常关机后找回的碎片文件只有 ext 系列文件系统才有这个目录/media自动挂载的外接设备与/mnt这个手动挂载点区分开/opt额外安装的第三方软件与/usr/local的取舍要能讲一句/lib共享库与内核模块编译时-l找的库、ldd看的依赖都在这一带/proc内核与进程的运行时信息虚拟文件系统不占磁盘读它等于问内核/tmp临时文件可能被清理别把要交的东西放这儿整理下来会发现只有/proc是个「假目录」里面看不到真实扇区读一次文件就触发一次内核现场计算。理解了这一点后面观察cpuinfo、meminfo才不会把它们当成静态配置文件。2.2 终端四件事切换、查看、进程、权限cd、ls、ps、chmod这四个命令必须能说清语义而不只是会用。ls -l输出的十位串第一位是文件类型-普通文件、d目录、l符号链接后九位按「属主、属组、其他」三组各三位rwx排布chmod 644 file与chmod urw,gor file是同一件事的两种写法前者是八进制位权后者是符号法。ps -ef与ps aux都能列进程前者偏重 UID、PID、CMD后者多出%CPU、%MEM做资源观察时用后者更顺手。cd -回到上一个目录这个小技巧在深路径里来回跳时能省很多时间。重定向和管道是把单条命令拼成流水线的关键覆盖写、追加、2单独接错误流、合并两路、|把前一条的 stdout 交给后一条的 stdin。举个实验里天天用的组合# 列出当前目录所有 .c 文件逐个搜含 fork 的行正常结果写文件报错单独留档 ls *.c | xargs grep -n fork fork_lines.txt 2 err.log这里的xargs负责把文件名列表转成grep的参数只接收标准输出2把「文件不存在」这类错误信息隔离到另一个文件——如果不分开错误信息会混进结果文件后面排查时就分不清哪行是真结果。管道只传 stdout这一点是笔试常考点也是很多人第一次写脚本时踩的坑。2.3 vim、gcc、gdb从源码到可执行文件的四步编辑器、编译器、调试器是一条固定链路缺一环都跑不起来vim argc.c # i/o/a 进插入模式Esc 回普通模式:wq 保存退出 gcc -g -Wall -o argc argc.c # -g 保留调试符号给 gdb 用-Wall 打开常用告警-o 指定输出名 chmod x argc # 权限不足时补一步或直接用 ./argc 运行 gdb ./argc # b main 下断点r 运行n 单步p 变量 打印几个参数值得记住不带-g编译出来的程序在gdb里看不到源码行只能看汇编-Wall会提示「变量声明了没用」「函数没有返回值」这类低级错误实验报告里那些莫名其妙的段错误一半能靠它提前发现。想在 vim 里另存为新文件名用:w newname.c已经退出编辑器后改名用mv argc.c oslab1.c。这两种改名方式经常被混着问。2.4 /proc把内核变量当文件读/proc下有两类内容一类是以数字命名的子目录每个数字是一个正在运行的进程号里面记录该进程的status、maps、fd等信息另一类是固定名字的全局文件每个文件对应一组内核变量。文件能读到什么常见用途/proc/cpuinfo处理器型号、核数、缓存、频率判断机器规格、看是否开了超线程/proc/version内核版本与编译信息版本号偶数一般是稳定版奇数是开发版/proc/statCPU 各态时间、中断、上下文切换算 CPU 使用率的原始数据/proc/uptime系统已运行秒数与空闲秒数配合 jiffies 理解计时机制/proc/meminfo总内存、空闲、缓存、交换观察内存压力单位是 kB/proc/loadavg1、5、15 分钟平均负载判断系统是否过载/proc/interrupts每个 CPU 上各中断号的触发次数看设备是否在正常发中断/proc/filesystems当前内核支持的文件系统类型挂载前的可行性检查uptime背后是 jiffies内核里一个全局变量每个时钟节拍加一开机时用 RTC 读到一个基准时间换算成距 1970-01-01 00:00:00 UTC 的节拍数存进去之后系统时间就是「基准节拍 当前 jiffies」。这套换算在讲进程调度和缺页统计时还会再出现因为内核里几乎所有「时间」都是用节拍表示的。用 C 程序读这些文件并不复杂关键是把「打开—逐行读—关闭」这条固定套路写对/* 只取 meminfo 里的两个关键字段观察单位与口径 */ #include stdio.h #include string.h int main(void) { FILE *fp fopen(/proc/meminfo, r); /* 虚拟文件系统open 时内核才生成数据 */ char line[256]; if (!fp) { perror(fopen); return 1; } while (fgets(line, sizeof(line), fp)) { /* 每行格式固定为 键: 数值 kB */ if (strncmp(line, MemTotal, 8) 0 || strncmp(line, MemFree, 7) 0) fputs(line, stdout); } fclose(fp); /* 关闭句柄后数据失效下次读要重新 open */ return 0; }fgets按行读比fscanf稳妥因为/proc里字段数量随内核配置变化按行切再自己解析更抗变化。这里最容易忽略的是单位meminfo里除少数几个字段外数值单位都是 kB直接当字节用会差三个数量级。2.5 虚拟机网络三模式为什么实验环境选 NAT做实验绕不开虚拟机联网三种模式的连通方向完全不同理解错了会浪费一整晚在 ping 不通上。模式虚拟机能否访问外网宿主机能否访问虚拟机IP 来源典型问题桥接能能与宿主机同网段的独立地址同网段设备多时容易地址冲突NAT能视端口转发配置而定由虚拟网络分配可不同网段外部设备主动连不进来仅主机不能能内部网段与真实网络隔离装包会失败桥接相当于把物理网卡变成一台交换机虚拟机是挂在交换机上的另一台机器地址同段、互相可见代价是占地址、易冲突。NAT 让虚拟机借宿主机出口访问外部出口地址是宿主机的冲突概率低代价是外部无法主动发起连接。仅主机模式把两者彻底隔离适合做纯本地测试。实验环境一般选 NAT既能装编译工具链又不用管地址规划。这套划分答简答题时写清「连通方向 地址来源」两点就够。3. 进程主线fork、信号、调度与互斥3.1 fork 的返回值决定你站在哪条分支fork()调用一次返回两次这是进程实验里最反直觉的一点。父进程拿到子进程的 pid大于 0子进程拿到 0出错返回 -1。判断分支时先判 0还是先判 0都不影响正确性但会影响你对进程树的直觉代码里同一段文字被两个进程各执行一遍如果不加区分地printf屏幕上就会出现两份输出。创建进程树时还要注意回收。父进程不wait()子进程退出后会留下僵尸条目ps里状态列显示Z。实验里常见写法是父进程发完信号后连续两次wait(0)对应它创建的两个子进程如果子进程数量不确定用while (wait(NULL) 0);更稳。3.2 信号用名字而不是裸数字控制进程树软中断通信实验的核心是「父进程捕获终端按键 → 转化为信号 → 通知子进程退出」流程可以压缩成一段能跑的骨架#include stdio.h #include signal.h #include unistd.h #include stdlib.h volatile sig_atomic_t wait_mark 1; /* 处理函数里只改标志位别做打印等不可重入操作 */ void stop(int sig) { wait_mark 0; } /* 信号一到就把循环条件打掉 */ int main(void) { pid_t p1 fork(); /* 父进程得 pid子进程得 0失败得 -1 */ if (p1 0) { perror(fork); return 1; } if (p1 0) { /* 父进程分支 */ signal(SIGQUIT, stop); /* Ctrl\ 发 SIGQUIT父进程先挂上处理函数 */ while (wait_mark) pause(); /* pause 让出 CPU 等信号比空转省资源 */ kill(p1, SIGUSR1); /* 用有名字的信号而不是 16、17 这种裸数字 */ wait(NULL); /* 回收子进程避免留下僵尸 */ printf(parent %d exit\n, getpid()); } else { /* 子进程分支 */ signal(SIGQUIT, SIG_IGN); /* 忽略 Ctrl\保证退出顺序由父进程掌控 */ signal(SIGUSR1, stop); while (wait_mark) pause(); printf(child %d exit\n, getpid()); } return 0; }几个点值得说透。signal(SIGQUIT, SIG_IGN)的作用是让子进程对终端按键免疫否则在终端按一次 Ctrl\整个进程组一起死就看不到「依次终止」的顺序了。忙等while (wait_mark);与pause()的区别在于后者会把 CPU 让出去实验报告里如果写「用 while 循环等待」答辩时很可能被追问为什么不 sleep。孙进程结构只是把fork再嵌一层父创建子、子创建孙退出顺序反过来先杀最深的那个再逐层往上。这里有个血泪经验原报告里用16、17当自定义信号编号。在常见的 x86 Linux 上17 恰好对应SIGCHLD子进程一退出就可能触发导致「子进程还没收到父进程的命令就自己结束了」这种玄学现象。规范写法是SIGUSR1、SIGUSR2编号由系统头文件给不同架构都不出错。3.3 输出交错与 lockf临界区不止线程才有多进程同时往标准输出写字符会交错成半行半行的乱码因为printf内部是「格式化到缓冲区 写 fd」两段中间可能被切走。lockf(1, 1, 0)就是给文件描述符 1stdout加锁三个参数分别是描述符、加锁/解锁标志、加锁长度0 表示从当前偏移到文件末尾解锁写lockf(1, 0, 0)。这一段在实验里往往只占两行但它解释了为什么结果图里有的行完整、有的行被撕开。3.4 FCFS 与 RR调度指标到底怎么算调度实验一般是给定一组进程的到达时间与服务时间手工算出完成时间、周转时间、带权周转时间再用程序验证。以三个进程为例P1 到达 0 服务 5P2 到达 1 服务 3P3 到达 2 服务 1。先看先来先服务FCFS按到达顺序执行P1 在 0–5P2 在 5–8P3 在 8–9。进程到达服务完成周转完成−到达带权周转周转/服务P105551.00P213872.33P321977.00平均周转时间约 6.33平均带权周转约 3.44。再看时间片轮转RRq1t0 只有 P1跑 1 个时间片剩 4t1 时 P2 到达队列为 P1、P2P2 跑完一片剩 2t2 时 P3 到达队列变为 P1、P3、P2P3 在 3–4 跑完完成时间 4接着 P2 在 4–5、P1 在 5–6、P2 在 6–7 结束、P1 在 7–9 结束。进程完成周转带权周转P1991.80P2762.00P3422.00平均周转约 5.67平均带权约 1.93。对比下来RR 的平均周转只略好一点但带权周转明显更优原因是短作业 P3 没被长作业堵在后面。这就是「响应比」的直观意义也是简答题里最常被要求解释的一句话。3.5 线程同步互斥量保护的是「读—改—写」三步线程实验里最经典的翻车场景是多个线程对同一个计数器累加结果比预期小。原因不在加法本身而在counter被编译成「取数—加一—写回」三条指令中间可能被切换。互斥量保护的正是这三步的原子性#include pthread.h pthread_mutex_t m PTHREAD_MUTEX_INITIALIZER; int counter 0; void *worker(void *arg) { for (int i 0; i 100000; i) { pthread_mutex_lock(m); /* 进入临界区其他线程在此阻塞 */ counter; /* 读-改-写三步必须整体保护 */ pthread_mutex_unlock(m); /* 离开临界区唤醒等待者 */ } return NULL; }需要强调的是粒度和配对锁住的范围太大退化成串行太小则保护不到lock与unlock必须成对中间return或异常退出就会死锁。条件变量配合互斥量实现生产者—消费者时判断条件要用while而不是if防止虚假唤醒后直接往下跑。4. 内存主线分配、系统调用与缺页计数4.1 从 malloc 到页框三层结构要能分开讲内存管理实验一般做两件事模拟分配算法的数据结构与回收规则观察真实系统的页行为。用户态看到的malloc/free是库函数小片内存走堆区、通过brk扩展大块直接用mmap映射再往下是页表把虚拟地址翻译成物理页框最底层才是物理内存。三个层次对应三套术语答题时混着用就是丢分点。模拟分配时常用的三种算法对比如下算法查找方式优点缺点首次适应从头找第一个够大的空闲块快低地址优先保留大块低地址碎片累积最佳适应找能满足的最小空闲块剩余碎片尽量小产生大量难以利用的小碎片最坏适应找最大的空闲块切分切完仍可用大块很快被消耗光回收时的关键是合并释放的块如果与前后空闲块地址相邻必须合并成一个更大的块否则空闲表里会堆满碎片后续大请求全部失败。这一步在代码里往往只有几行判断但它是「内存分配是否可用」的分水岭。4.2 系统调用从用户态陷入到内核函数新增一个系统调用的流程是固定的五步在内核源码里实现功能函数在系统调用表中按编号登记函数指针在公开头文件里补上编号宏重新编译并安装内核用户态用syscall()或封装好的库函数验证。用户态发起调用的写法是这样#include unistd.h #include sys/syscall.h long pid syscall(__NR_getpid); /* 编号来自 sys/syscall.h参数按 ABI 顺序跟在后面 */syscall会把编号放进约定寄存器然后触发陷入指令切到内核态内核按编号查表跳到对应函数返回值经寄存器带回失败时返回 -1 并设置errno。理解这条链路就能解释为什么系统调用的开销明显大于普通函数调用模式切换、参数校验、可能的数据拷贝每一步都要花钱。4.3 缺页统计两种口径不要混用缺页次数有两条获取路径。第一条是读/proc/vmstat里的pgfault与pgmajfault这是整机自开机以来的累计值第二条是用getrusage取当前进程的ru_minflt与ru_majflt这是单进程维度的累计值。#include stdio.h #include stdlib.h #include sys/resource.h int main(void) { struct rusage before, after; getrusage(RUSAGE_SELF, before); /* 先记基准后面算增量才有意义 */ size_t n 1 20; /* 约 4MB 的 int 数组 */ int *p malloc(n * sizeof(int)); for (size_t i 0; i n; i) p[i] i; /* 首次写每一页都会触发一次缺页 */ getrusage(RUSAGE_SELF, after); printf(minor%ld major%ld\n, after.ru_minflt - before.ru_minflt, /* 轻微缺页页已在内存补映射即可 */ after.ru_majflt - before.ru_majflt);/* 重大缺页需要读磁盘或换页 */ free(p); return 0; }两个字段的区别必须说清轻微缺页只是页表项缺失物理页可以立刻给比如零页、写时复制、页缓存命中重大缺页要等磁盘 I/O数量级完全不同。实验里如果只报一个「缺页次数」而不区分两者结论基本没法用。另外测增量一定先取基准值直接打印累计值会把进程启动时的开销也算进去数字看着大其实跟你的循环没关系。4.4 三个实验怎么串成一条线系统调用、内存分配、缺页统计放在一起看才有意义malloc只承诺虚拟地址空间真正占物理页是在第一次写的时候那一次写触发轻微缺页内核补上页表项如果内存紧张触发了换出再访问就是重大缺页代价是磁盘往返。系统调用则是你主动进入内核的入口brk、mmap都是它的具体形态。把这条链画清楚三个实验的结论就能互相印证而不是三份互不相干的报告。5. 易漏点清单五处最容易丢分的地方5.1 fork 之后的输出重复易漏点父进程创建子进程后缓冲区里的内容被复制一份父子各刷一次屏幕上出现重复行。为什么漏printf不是立即写设备而是先进用户态缓冲区fork复制的是整个地址空间缓冲区也在其中。怎么补在fork之前fflush(stdout)或者每条输出后加\n并确认终端是行缓冲判断分支时用if (pid 0) {...} else if (pid 0) {...}明确分隔。5.2 信号用裸数字注册易漏点signal(17, handler)这类写法在换机器或换架构后行为不一致。为什么漏编号与信号的对应关系由平台头文件决定不同架构上同一个数字的含义可能不同17 在常见 x86 上对应子进程状态变化信号很容易被子进程退出误触。怎么补一律用SIGUSR1、SIGUSR2这类有名字的常量需要自定义语义时在注释里写明用途测试时先用kill -l确认编号映射。5.3 把 /proc 里的数当成静态值易漏点连续两次读meminfo得到不同的值就以为程序写错了。为什么漏/proc是虚拟文件系统每次open都由内核现场生成内容数值本身就是瞬时快照同时MemFree不等于「可用内存」MemAvailable才更接近实际可分配量。怎么补报告里注明采样时刻与采样次数需要趋势就连采多次取区间单位统一按 kB 换算写结论时带上单位。5.4 缺页计数口径混用易漏点把轻微缺页和重大缺页加在一起报一个总数或者拿整机/proc/vmstat的累计值当成本次实验的增量。为什么漏两个数据源一个按进程、一个按整机一个是累计、一个需要作差字段名又很像。怎么补实验前先getrusage取基准结束后作差结论里分开写minflt与majflt并说明本次访问模式为什么会让两者比例失衡。5.5 调度指标算错起点易漏点周转时间用「完成时间减开始执行时间」算带权周转忘了除服务时间。为什么漏手算时容易盯着甘特图上的执行区间忽略进程可能在到达后等了很久带权周转是比值量纲不同忘记除就会出现「带权周转比周转还小」的反常结论。怎么补固定写三列——到达、完成、服务周转一律用完成 − 到达带权一律用周转 / 服务算完检查一次「带权周转 ≥ 1」是否全部成立不成立就是算错了。6. 复习清单把八次实验压成一张可自查的表复习到后期最有用的不是再读一遍报告而是拿一张表逐项自问「这个函数我能不能默写、这个数字我能不能解释」。下面这张表是我自己按实验顺序整理的用来做最后一轮查漏补缺实验必须能默写的核心必须能解释的一句话环境与调试gcc -g -Wall -o、gdb的b/r/n/p不加-g为什么看不到源码行目录与命令ls -l十位串、chmod两种写法重定向与管道分别传什么流/proc 观察逐行读文件的固定套路为什么/proc不占磁盘软中断通信fork、signal、kill、wait子进程为什么要忽略 Ctrl\进程调度FCFS 与 RR 的周转、带权周转为什么 RR 的带权周转更小线程同步pthread_mutex_lock/unlock配对为什么循环判断要用while内存管理首次/最佳/最坏适应的差异回收时为什么必须合并相邻块系统调用五步流程与syscall()写法系统调用比函数调用贵在哪缺页统计getrusage取增量minflt与majflt差在哪具体怎么用这张表先合上资料对着「必须能默写」那列在纸上写代码骨架写不出来的立刻标红再对着「必须能解释」那列出声讲一遍讲到卡壳的地方就是真正的漏洞。第二轮只攻标红项第三轮把九行连起来讲成一条完整链路——从敲命令读/proc到fork出进程树到调度算法决定谁先跑到内存分配触发缺页最后用系统调用收尾。还有两个自查小技巧。一是画图进程树用手画标上 pid 和信号流向内存分配画空闲块链表标出每次分配后碎片的位置。图能画对代码基本不会写错。二是对数跑完程序别急着截图先看/proc里的数字量级对不对——4MB 数组首次写入轻微缺页数量应该在千次量级如果报出几十万次八成是把累计值当增量了。这种「先核对再下结论」的习惯比多刷两套题管用。那份实验报告我是当索引用的卡在某个实验的某个函数上就翻到对应章节看它的代码骨架和结果分析对着改参数、改进程数看输出怎么变比从头读一遍效率高得多。需要的话可以拿去对着自己的实验逐条比对。从那以后我每次做系统实验都强制先画一张进程树或内存流程图再动手写代码跑完先不看输出、先去/proc里核对一眼数字量级翻车的次数少了不止一半。希望帮到你。本文还有配套的精品资源点击获取