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-essentialbuild-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 -u0 是子进程拿到的返回值表示“你回到了 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 返回时的分支判断让子进程在 pid0 的分支里继续往下创建每层只创建一个子进程。#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: pid1000 level 3: pid1001, ppid1000 level 2: pid1002, ppid1001 level 1: pid1003, ppid1002三个 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 是 ZCMD 后面多了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-essentialsudo 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 traceprocess -o trace.txt ./exp1 head -50 trace.txt这里解释一下参数。-f 表示跟踪 fork 出来的子进程不加的话只能看到父进程的系统调用-e traceprocess让输出只保留进程管理的系统调用避免被大量文件读写刷屏-o 把结果写到文件避免和程序自己的输出混在一起。打开 trace.txt 能看到类似内容execve(./exp1, [./exp1], 0x7ffe...) 0 clone(child_stackNULL, flagsCLONE_CHILD...) 12345 wait4(12345, 0x7fff..., 0, NULL) 12345 exit_group(0)对照报告可以写程序入口 execve 加载可执行文件fork 在系统调用层表现为 clonewait 表现为 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 21 | grep -E Elapsed|Maximum done三次数据差不多说明 fork 开销不会随运行次数漂移。注意不要在报告里堆大量无关数字两个核心字段加一句分析就够。5.3 报告配表核心代码、运行截图、文字分析怎么对应操作系统实验报告的评分点一般落在这几块实验目的、实验环境、设计思路、核心代码、运行结果、结果分析、问题与心得。最容易写砸的是“代码 截图 一句看懂了”因为材料之间没有对应起来。用一张表把逻辑串起来报告章节内容配套材料实验环境Ubuntu 22.04gcc 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 的宏、调度顺序、系统调用轨迹这些细节写进分析里才说明你理解了操作系统这门课在讲什么。希望帮到你。本文还有配套的精品资源点击获取
返回列表