拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

操作系统实验报告一:Linux进程控制与fork/wait原理详解

操作系统实验报告一:Linux进程控制与fork/wait原理详解

简介:一份面向操作系统课程实验的完整资料包,围绕优先权调度算法模拟展开。实验设定6个进程并用类似PCB的数据结构表示,输入优先权与运行时间后按优先权降序构建就绪队列;调度器每次选取队首进程,运行一次优先权减1、运行时间减1,直至剩余时间为0则进程结束并退出队列,从而完整呈现处理机调度的动态变化过程。压缩包共2个文件,其中C语言源程序实现链表式就绪队列与调度循环,Word实验报告逐步说明设计思路、数据结构与运行结果,整体仅593KB,轻量便于下载使用。已有791人浏览学习,适合操作系统初学者对照报告理解进程控制块组织、链表就绪队列及优先权调度机制;也可作为实验设计、课程设计或复习备考的参考,便于对照完整代码与报告理解动态优先权调度的实现细节。

1. 天津理工大学操作系统实验报告一+代码:先弄清楚这份实验真正在考什么

把“天津理工大学操作系统实验报告一+代码”这几个关键词拆开看,核心其实就两件事:第一,在 Linux 环境里完成一个进程控制相关的 C 语言实验;第二,把代码、运行截图和实验分析整理成一份老师看得下去的报告。很多同学卡住不是不会写代码,而是不知道实验报告里哪些地方需要展开、哪些地方一图带过。这篇笔记就沿着实验一最常见的实现路径——进程创建、进程回收、系统调用观察——从环境搭建讲到报告排版,新手能照着敲,熟手可以直接跳去看避坑章节。

2. 实验一核心链路:从 Ubuntu 虚拟机搭建到 fork/wait 最小代码跑通

2.1 环境选型:为什么我推荐 VMware 虚拟机加 Ubuntu LTS

实验一需要的环境并不复杂:一个能运行 Linux 的机器、一个 gcc 编译器、一个能截图的终端。但就是“环境”这一步能卡掉一半人。常见做法是在 Windows 宿主机上装 VMware Workstation Player(个人使用免费),再装一个 Ubuntu 20.04 或 22.04 LTS 虚拟机。之所以不推荐直接在物理机装双系统,是因为实验过程中你会反复编译、反复跑进程观察,双系统一旦引导出问题,整台电脑都受影响;而虚拟机可以随手拍快照,改坏了还原回去,相当于给自己留了后悔药。

虚拟机配置建议:内存给 2GB 以上,硬盘 20GB 足够,CPU 给 2 核。实验一不涉及重负载,但如果你还要在虚拟机里跑 VSCode 远程开发,内存低于 2GB 会明显卡顿。装完 Ubuntu 后先做两件事:更新软件源,安装编译工具链。命令如下:

sudo apt update sudo apt install -y build-essential

build-essential 包含了 gcc、g++、make 等工具,几乎覆盖实验一所有编译需求。装好后用下面两条命令验证环境,并把输出截图留底,报告里的“实验环境”章节直接引用这份截图:

uname -a gcc --version

有同学会在启动虚拟机时碰到提示“客户机操作系统已禁用 CPU,请关闭或重置虚拟机”。这多半不是镜像问题,而是宿主机 BIOS 里 Intel VT-x / AMD-V 没打开,或者虚拟机设置里没有启用虚拟化引擎。解决方法是重启进 BIOS,打开虚拟化选项;如果已经开了,就在虚拟机的处理器设置里勾选“虚拟化 Intel VT-x/EPT 或 AMD-V/RVI”,然后关机再开机。这个坑和实验内容无关,但会影响你所有后续实验,第一次就处理好最省事。

2.2 最小可运行代码:fork/wait 骨架与每行参数含义

实验一无论题目怎么变,“创建子进程并回收”是最小场景。先给一份能跑通的骨架,把 fork 和 wait 的用法吃透,再做扩展。

#include <stdio.h> #include <unistd.h> #include <sys/wait.h> #include <sys/types.h> int main() { pid_t pid = fork(); // 一次调用,两次返回 if (pid < 0) { perror("fork failed"); // 创建失败,常见原因是进程数达上限 return 1; } else if (pid == 0) { // 子进程进入这个分支 printf("[child] pid=%d, ppid=%d\n", getpid(), getppid()); return 42; // 子进程以退出码 42 结束 } else { // 父进程拿到子进程 PID,大于 0 int status; waitpid(pid, &status, 0); // 0 表示阻塞等待该子进程结束 if (WIFEXITED(status)) printf("[parent] child exit code: %d\n", WEXITSTATUS(status)); } return 0; }

编译和运行:

gcc -o exp1 exp1.c -Wall ./exp1

这段代码里的参数要看清。fork 返回值是三态的:负数说明创建失败,检查内存或进程数上限ulimit -u;0 是子进程拿到的返回值,表示“你回到了 fork 调用点,但你现在是子进程”;大于 0 是父进程拿到的返回值,数值就是子进程 PID。子进程结束我习惯用_exit(0)而不是exit(0),因为_exit不刷新 stdio 缓冲区,能规避后面要讲的缓冲区复制问题。

waitpid 第一个参数 pid 表示等待目标,填 -1 表示等任意子进程;第二个参数 status 是输出型参数,保存子进程终止状态;第三个参数是选项,0 表示阻塞等待,WNOHANG 表示非阻塞。实验一阶段填 0 就够了,但 WNOHANG 在后续 Shell 实验里很常见,提前记下来。WIFEXITED 和 WEXITSTATUS 是状态宏,前者判断进程是否正常退出,后者取出低 8 位退出码。如果子进程是被信号杀掉的,WIFEXITED 为假,此时要看 WTERMSIG。

如果题目里明确包含 exec 系列调用,可以在子进程分支里这样替换当前进程镜像:

} else if (pid == 0) { execl("/bin/ls", "ls", "-l", NULL); perror("execl failed"); // 只有失败才会执行到这里 _exit(127); }

execl 成功时没有返回值,因为当前进程的用户态代码已经被新程序覆盖了;只有失败才返回 -1,所以下面一定要跟 perror 和退出码。报告里可以对比 fork 和 exec 的区别:fork 复制当前进程,exec 替换当前进程,两者经常配合使用。

2.3 跑通后立刻做三个观测,写到报告的结果分析里

第一个观测是 PID 关系。每次运行得到的 child 行和 parent 行里,子进程 PID 和父进程 PID 相差多少?多数 Linux 发行版上,fork 出的子进程 PID 大于父进程,但并非严格连续,因为进程号按位图分配,存在回绕。报告不用写太深,但要写清“子进程 PID 是当时下一个可用号”这个观察。

第二个观测是输出顺序。多跑几次,你会看到 child 和 parent 的打印顺序可能不同。这不是代码 bug,而是父进程和子进程在 fork 返回后同时进入运行队列,谁先抢到 CPU 由调度器决定。这个结论直接对应“进程并发执行”的概念,建议单独写一段,比贴代码更值分。

第三个观测是退出码。父进程打印 child exit code: 42,说明 wait 真正拿到的子进程终止状态。你可以改成一个 0-255 的整数再跑一次,注意 Linux 退出码只取低 8 位,写 300 会变成 44。这个边界知识很适合写进“问题与心得”。

3. 把报告做出亮点:进程树、僵尸进程与调度切换观测

3.1 用 fork 构建三层进程树,打印亲子关系

实验一扩展题最常见的就是“创建一棵指定层数的进程树”。很多初写者用循环连续 fork,结果进程数量变成 2 的 n 次方,树完全失控。正确做法是借助 fork 返回时的分支判断,让子进程在 pid==0 的分支里继续往下创建,每层只创建一个子进程。

#include <stdio.h> #include <unistd.h> #include <sys/wait.h> void make_tree(int depth) { if (depth == 0) return; pid_t pid = fork(); if (pid < 0) { perror("fork failed"); return; } if (pid == 0) { printf("level %d: pid=%d, ppid=%d\n", depth, getpid(), getppid()); make_tree(depth - 1); // 子进程继续往下层创建 _exit(0); } wait(NULL); // 父进程等直接子进程结束再返回 } int main() { printf("root: pid=%d\n", getpid()); make_tree(3); return 0; }

运行后的输出类似:

root: pid=1000 level 3: pid=1001, ppid=1000 level 2: pid=1002, ppid=1001 level 1: pid=1003, ppid=1002

三个 level 的 PPID 链构成一条从根到叶的路径。要验证这棵树确实是父子关系,可以在程序里加一行sleep(1),趁进程还活着时在另一个终端执行:

ps -ef --forest | grep exp_three

--forest会用树形结构显示父子关系,截图放到报告里比任何文字都直观。这个递归写法的优点是 depth 参数控制层次,不会出现 fork 炸弹。

3.2 主动制造一个僵尸进程,观察再回收

报告里写“僵尸进程”概念比较空,不如直接在代码里制造一个给老师看。做法很简单:子进程先退出,父进程故意等几秒再调用 wait。

#include <stdio.h> #include <unistd.h> #include <sys/wait.h> int main() { pid_t pid = fork(); if (pid == 0) { printf("child pid=%d, about to exit\n", getpid()); _exit(0); } else { printf("parent pid=%d, sleeping 10s before wait\n", getpid()); sleep(10); int status; waitpid(pid, &status, 0); printf("recycled child, exit code=%d\n", WEXITSTATUS(status)); } return 0; }

编译运行后,在父进程 sleep 的 10 秒里打开另一个终端,执行:

ps -o pid,ppid,stat,cmd | grep a.out | grep -v grep

你会发现子进程那行 STAT 是 Z,CMD 后面多了<defunct>标记。这就是僵尸进程:它已经结束,资源都被回收了,只留下一个进程表项,等父进程来读取退出状态。僵尸进程不能用 kill -9 杀掉,因为它的生命已经结束,能做的只有等父进程 wait,或者等父进程也退出后由 init 进程收养并回收。

这个实验的价值在于把“进程状态”从教材里的状态转换图变成可见现象。报告写法建议:贴一张 ps 截图,再贴一张 wait 之后 “recycled child” 的截图,文字说明从 Z 状态到消失的过程。想继续加分,可以在父进程里忽略 SIGCHLD,让 Linux 自动回收子进程,对比观察一次。

3.3 用循环 fork 观察调度切换:打印顺序为什么像玄学

第三个加分点是并发观察。写一个循环创建五个子进程,每个子进程只打印自己的 PID:

#include <stdio.h> #include <unistd.h> #include <sys/wait.h> int main() { for (int i = 0; i < 5; i++) { pid_t pid = fork(); if (pid == 0) { printf("child %d from parent %d\n", getpid(), getppid()); _exit(0); } } // 父进程在循环外统一回收所有子进程 while (wait(NULL) > 0) ; return 0; }

这里有个关键细节:父进程不能在 for 循环里 wait,否则它会等第一个子进程退出后再创建下一个,整段程序退化成串行。正确做法是先把 5 个子进程全部创建出来,然后在外层 while 循环里统一回收。

多跑几次,对比输出,会发现五个子进程的打印顺序每次都不同。这不是程序不稳定,而是调度器在起作用:多个就绪进程竞争 CPU,谁先被调度没有硬性保证。如果你发现输出顺序总是整整齐齐,要回去检查代码,是不是子进程里漏了 _exit,导致子进程继续执行 for 循环又 fork 了一遍,后面避坑章会专门讲。

4. 避坑:实验报告一里五个高频翻车点与排查方法

4.1 现象:fork 之后 printf 打了两遍

写完最小代码,运行发现 fork 后面的 printf 输出了两次,第一反应是系统坏了。原因在 C 标准库的 IO 缓冲:printf 先写进用户态缓冲区,fork 复制进程内存时把缓冲区内容也复制了一份,于是同一段缓冲被父子进程各输出一次。

解决有两个方向:一是让 printf 以换行符结尾,终端上通常会触发行缓冲清理,但重定向到文件时不保证;二是 fork 前调用fflush(NULL)清空所有缓冲。实验代码里更推荐直接用 write(1, "...", 长度),write 是系统调用,不经过 stdio 缓冲。这个现象写进报告“问题与心得”反而是加分项,说明你踩过缓冲复制的坑。

4.2 现象:ps 里堆了一堆<defunct>

子进程频繁创建退出,父进程没有及时 wait,进程表项就会堆积成僵尸。原因不是内存泄漏,而是 wait 路径没覆盖所有子进程,比如 wait 放在 for 循环里只等了一个,或者父进程一直不退出。

排查命令:

ps -o stat,cmd | grep -E 'a.out|Z'

如果第二列状态是 Z,基本就是僵尸进程堆积。解决方式是确认 wait 覆盖了全部子进程,用while(wait(NULL) > 0)循环回收,或者用waitpid(-1, &status, 0)。如果你在代码里写了signal(SIGCHLD, SIG_IGN)忽略子进程退出信号,Linux 会自动回收子进程,一般不会出现僵尸;但实验如果要求观察 wait 行为,就别加这行。

4.3 现象:gcc 编译报错,报错信息全英文看不懂

最常见的是fatal error: sys/wait.h: No such file or directory。这在 Ubuntu 上说明系统里没有完整 C 开发头文件,只有精简版 libc。解决方法是补装 build-essential:

sudo apt update sudo apt install -y build-essential

如果是undefined reference to wait,多半是头文件没包全,或者函数名写成了 wait()。Linux 里 wait 是系统调用的封装,正确写法是wait(&status)或waitpid(pid, &status, 0)。gcc 加-Wall可以提前暴露隐式声明警告。

另一个高频警告是 pid_t 打印格式。pid_t 在 x86_64 上实际是 int,为了让代码在 32 位和 64 位系统上都干净,建议这样写:

printf("pid=%ld\n", (long)pid);

4.4 现象:报告里的运行截图和实验环境对不上

有人代码在 Ubuntu 里写,截图却是 Windows PowerShell,背景一看就是 Mac 终端。这种细节老师一眼就能看出来,轻则扣环境分,重则质疑整份报告真实性。

解决方法是统一操作环境。所有编译、运行、查进程都在 Ubuntu 虚拟机里完成,截图前先执行uname -a和gcc --version,把输出留在终端里再截下一张图,这样每张截图自带环境证据。建议在虚拟机里固定一个工作目录,比如~/oslab/exp1,报告里写清楚路径,复审时能按步骤复现。

4.5 现象:同一份代码运行两次结果不同,被老师质疑代码有问题

这其实是并发实验的常见预期。父进程和子进程谁先调度没有硬性保证,print 到终端的顺序自然不确定。不要为了截图一致去伪造输出,更不要在代码里硬编码 sleep 来强行排序。

正确做法是在报告里放两次运行截图,并写一句:“第二次运行时 child 3 先打印,第一次是 child 5 先打印,说明操作系统对就绪进程的调度顺序不固定,这正是实验要观察的并发现象。”把“结果不一样”翻译成“调度非确定性”,从扣分项变成加分项。

5. 从能过到加分:用 strace、time 和报告配图把实验分析做实

5.1 用 strace 抓系统调用顺序,给实验分析一张硬证据

实验报告的“原理分析”部分光写文字说服力不够,如果能贴一张系统调用记录,老师会默认你真跑过。strace 是 Linux 下追踪系统调用的工具,安装和用法都很简单:

sudo apt install -y strace strace -f -e trace=process -o trace.txt ./exp1 head -50 trace.txt

这里解释一下参数。-f 表示跟踪 fork 出来的子进程,不加的话只能看到父进程的系统调用;-e trace=process让输出只保留进程管理的系统调用,避免被大量文件读写刷屏;-o 把结果写到文件,避免和程序自己的输出混在一起。

打开 trace.txt 能看到类似内容:

execve("./exp1", ["./exp1"], 0x7ffe...) = 0 clone(child_stack=NULL, flags=CLONE_CHILD...) = 12345 wait4(12345, 0x7fff..., 0, NULL) = 12345 exit_group(0)

对照报告可以写:程序入口 execve 加载可执行文件,fork 在系统调用层表现为 clone,wait 表现为 wait4。这三个点能对应操作系统教材里的进程创建和进程同步章节。strace 输出不用全贴,截取 fork 前后的 10 行足够,图注写明“红色框内是 clone 系统调用”更直观。

要注意一点:strace 里的 PID 不是终端输出的 PID 那种叫法,每行开头的数字是跟踪进程的 PID。带 -f 后,子进程系统调用行会换一个 PID 开头,这正好可以解释父进程和子进程是两个独立上下文。报告分析里能把这个看明白,水平一下就上来了。

5.2 用 /usr/bin/time 量化 fork 开销,实验分析里多一句数据

报告里写“进程创建耗时约 0.001 秒”比只写“fork 很快”更有说服力。time 命令有两个版本:shell 内建 time 输出简单,不支持细分资源指标;外部命令/usr/bin/time支持 -v 输出详细资源占用:

/usr/bin/time -v ./exp1 2> time.txt cat time.txt

注意2>重定向,time 的结果写到 stderr。输出里重点看这几项:

Elapsed (wall clock) time (h:mm:ss or m:ss): 0:00.01 User time (seconds): 0.00 System time (seconds): 0.00 Maximum resident set size (kbytes): 2312

报告中可以写:父进程阻塞等待期间,进程最大驻留内存约 2.3 MB,说明 fork 创建子进程时并非全量复制地址空间,而是利用页表共享配合写时复制。这一句就把性能现象和操作系统原理连起来了。

想多采几个样,可以用一个简单循环:

for i in 1 2 3; do /usr/bin/time -v ./exp1 2>&1 | grep -E "Elapsed|Maximum" done

三次数据差不多,说明 fork 开销不会随运行次数漂移。注意不要在报告里堆大量无关数字,两个核心字段加一句分析就够。

5.3 报告配表:核心代码、运行截图、文字分析怎么对应

操作系统实验报告的评分点一般落在这几块:实验目的、实验环境、设计思路、核心代码、运行结果、结果分析、问题与心得。最容易写砸的是“代码 + 截图 + 一句看懂了”,因为材料之间没有对应起来。用一张表把逻辑串起来:

报告章节内容配套材料
实验环境Ubuntu 22.04,gcc 11.4uname -a、gcc --version 截图
设计思路fork 创建子进程,wait 回收手绘进程状态转移图或文字描述
核心代码fork/wait/exec 关键片段只截取核心函数,不要贴整个文件
运行结果一次完整运行输出终端截图,前后各留一行命令历史
结果分析PID 变化、退出码、调度顺序两次运行截图对比,加 strace trace 片段
问题与心得缓冲区复制、僵尸进程、strace 观察对应 4.x 节里的诊断命令输出

截图有个小技巧:终端背景保持默认,字号调大,执行命令前先pwd,这样截图里能看到工作目录,和报告一致。代码截图如果用在深色主题,注意注释颜色对比度,打印出来常常看不清。报告篇幅控制在 8 到 10 页以内,核心代码放关键行,完整代码以小节附件形式提交,老师复查时能对得上就行。

6. 下次再写操作系统实验报告一,我直接照这张自查单来

把这些年踩过的坑浓缩成一张清单,每次交实验报告一之前过一遍。环境部分:Ubuntu 虚拟机快照是否打好,gcc 版本是否截图,工作目录是否固定。代码部分:fork 返回值三态是否都处理,wait 是否覆盖所有子进程,子进程是否用_exit避免 stdio 缓冲复制,exec 是否在失败后做了处理。结果部分:是否准备两张运行结果证明调度不确定性,退出码是否真的从 wait 状态宏里取出来。分析部分:是否提到了 Z 状态、clone/wait4 这些系统调用层对应。格式部分:代码是否只贴核心片段,截图路径是否清晰,是否用了 strace 和 time 的数据。

个人习惯是先把报告评分点列出来再动手写代码,这样不会为了凑篇幅贴代码。每次改完代码只动一个变量或一段结构,跑通后立刻复制一份带日期后缀的备份,比如 exp1_0330.c,改坏了随时回退。所有实验代码统一放在~/oslab/exp1,终端里执行 history 能直接翻到上次用的命令,写报告时不用重新回忆。

最后一句话送给准备交报告的人:代码能跑只说明你完成了,把 fork 的返回值、wait 的宏、调度顺序、系统调用轨迹这些细节写进分析里,才说明你理解了操作系统这门课在讲什么。希望帮到你。

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

返回列表