简介:本资源是天津理工大学操作系统课程的实验一完整实现包,面向计算机专业本科生及操作系统初学者,聚焦进程调度核心机制的理解与编程实践。实验基于优先权调度算法,通过C语言实现6个进程的PCB结构、就绪队列链表管理、动态优先权调整与运行状态追踪,并完整输出各轮调度过程中的进程名及数据结构变化,有效支撑课堂理论向代码落地的转化。压缩包共2个文件(1个C源码文件用于算法实现与调试,1个Word文档含规范实验报告、流程图、关键代码注释与运行结果截图),总大小593KB,轻量易解压,适合作为课程作业参考或自主复现练习。已有792人学习下载,内容结构清晰、代码可编译运行、报告格式规范,特别适合巩固进程控制块设计、链表操作及调度策略模拟等关键知识点。
1. 天津理工大学操作系统实验报告一:从 PCB 结构体落地到进程控制的完整闭环
你手头刚拿到《操作系统实验报告一》的 PDF,封面印着“天津理工大学 计算机科学与工程学院”,第一页写着“实验名称:进程控制——PCB 的设计与实现”,附件里有个pcb.c文件,但编译报错undefined reference to 'main';或者你正对着fork()调用后子进程输出乱序、父进程提前退出、资源未回收等问题抓耳挠腮——这不是代码写错了,而是你还没真正把“进程”从课本概念变成内存里可调试、可观察、可干预的实体。本报告的核心不是交差,而是用 C 语言在 Linux 环境下亲手构造一个最小可行的进程控制模型:定义 PCB(进程控制块)结构体、模拟进程创建/阻塞/唤醒/终止的生命周期、用fork()和信号机制实现父子进程协同,并通过ps、/proc文件系统和strace验证行为。它面向的是刚学完进程概念、但还没在真实系统里“摸过进程”的本科生,也适合想补足底层实践断层的转行开发者。不依赖任何 GUI 工具或教学平台,所有操作均可在 Ubuntu 22.04 或 CentOS 7 的终端中复现,关键在于理解pcb.c中每个字段为何存在、每行fork()后为何要waitpid()、为什么sleep(1)不是玄学而是调度可见性的锚点。
2. PCB 结构体设计:为什么必须包含 pid、state、priority、parent_pid 这四个字段?
2.1 进程状态机与 state 字段的不可替代性
操作系统中进程不是静态对象,而是一个具有明确生命周期的状态机:就绪(READY)、运行(RUNNING)、阻塞(BLOCKED)、终止(TERMINATED)。pcb.c中常见的enum { READY, RUNNING, BLOCKED, TERMINATED } state;不是装饰,而是调度器决策的唯一依据。例如,当内核检测到某进程因等待 I/O 进入阻塞态时,会立即将其从运行队列移出,放入等待队列;若缺少state字段,schedule()函数将无法判断该进程是否可被调度——它会盲目地把已阻塞的进程重新推上 CPU,导致系统假死。实际编码中,state必须配合原子操作更新(如__sync_fetch_and_add),否则多线程修改时会出现竞态:两个线程同时将state从 RUNNING 改为 BLOCKED,结果只执行了一次写入,进程状态丢失。我们不用锁,而用 GCC 内置原子函数确保状态变更的线性一致性。
2.2 pid 与 parent_pid:父子关系链是进程树的物理基础
pid_t pid; pid_t parent_pid;这两个字段共同构成 Linux 进程树的物理链路。pid是内核分配的唯一标识,parent_pid则记录创建它的父进程 ID。这个设计直接支撑了ps -ef的树形输出:ps命令读取/proc/[pid]/stat中的ppid字段(即parent_pid),再递归向上查找,最终渲染出systemd─┬─gnome-shell这样的层级。实验中若漏掉parent_pid初始化,waitpid(-1, &status, 0)将无法匹配子进程退出事件——因为内核需通过parent_pid定位哪个父进程应接收 SIGCHLD 信号。常见错误是fork()后在子进程中未重置parent_pid,导致子进程误认为自己仍是顶层进程,waitpid()在父进程中永远阻塞。
2.3 priority 字段:不是摆设,而是抢占式调度的量化依据
int priority;在教学实验中常被设为固定值(如 10),但它直指现代操作系统核心机制:优先级调度。Linux 的 CFS(完全公平调度器)虽以虚拟运行时间为调度依据,但nice值(-20 到 19)会映射为prio值参与计算。实验中若将priority设为 1(高优先级)和 100(低优先级),再用sched_setscheduler(0, SCHED_FIFO, ¶m)强制设置实时策略,就能观察到高优进程几乎独占 CPU:while(1) { printf("high\n"); sleep(1); }与while(1) { printf("low\n"); sleep(1); }并发运行时,“high” 输出频率远高于 “low”。这验证了priority不是理论参数,而是可被内核直接读取并影响调度决策的内存变量。
提示:
pcb.c中priority若声明为char类型(范围 -128~127),在nice值超出范围时会溢出,导致调度异常。务必使用int并在初始化时做边界检查:if (priority < 1) priority = 1; if (priority > 99) priority = 99;
3. 进程创建与生命周期管理:用 fork() 模拟 PCB 实例化与状态流转
3.1 fork() 后的三步黄金操作:waitpid()、exit()、资源清理
fork()创建子进程后,必须立即处理三件事,缺一不可:
#include <sys/wait.h> #include <unistd.h> #include <stdlib.h> pid_t pid = fork(); if (pid == 0) { // 子进程:执行业务逻辑 printf("Child process %d running\n", getpid()); sleep(2); // 模拟工作 exit(0); // 必须调用 exit(),而非 return! } else if (pid > 0) { // 父进程:等待子进程结束 int status; pid_t ret = waitpid(pid, &status, 0); // 阻塞等待指定子进程 if (ret == pid && WIFEXITED(status)) { printf("Child %d exited normally with code %d\n", pid, WEXITSTATUS(status)); } } else { perror("fork failed"); }exit(0)不是return 0:return仅退出当前函数,而exit()会触发内核清理:释放内存页、关闭文件描述符、向父进程发送 SIGCHLD。若子进程用return,它会继续执行后续代码(可能是父进程的逻辑),造成严重逻辑错乱。waitpid(pid, &status, 0)的pid参数必须精确:填-1表示等待任意子进程,但在单子进程实验中易导致父进程误等其他后台进程;填具体pid才能精准绑定父子关系。WIFEXITED(status)和WEXITSTATUS(status)是解析退出码的唯一安全方式:直接读status是未定义行为,不同架构返回值布局不同。
3.2 用信号模拟进程阻塞与唤醒:SIGSTOP/SIGCONT 的实战约束
实验报告常要求“模拟进程阻塞”,但sleep()只是时间阻塞,无法体现资源竞争阻塞。更贴近真实的方案是用kill()发送SIGSTOP使进程暂停,再用SIGCONT唤醒:
// 父进程创建子进程后 pid_t child = fork(); if (child == 0) { while(1) { printf("Child %d working...\n", getpid()); sleep(1); } } else { sleep(1); // 等子进程启动 kill(child, SIGSTOP); // 发送停止信号 printf("Child %d stopped\n", child); sleep(2); kill(child, SIGCONT); // 发送继续信号 sleep(3); kill(child, SIGTERM); // 终止 }但此操作有硬性约束:SIGSTOP和SIGCONT无法被进程忽略或捕获,这是内核强制行为;而SIGTERM可被捕获,若子进程注册了signal(SIGTERM, handler),则需在handler中调用exit()主动退出,否则进程会僵死。实验中若忘记SIGTERM后的waitpid(),子进程将变成僵尸进程(Zombie),ps aux | grep Z可查证。
3.3 PCB 数组管理:静态分配 vs 动态 malloc 的内存安全边界
教学代码常用struct pcb pcb_table[MAX_PROC]静态数组存储所有 PCB,但MAX_PROC设为 100 时,若实际创建 101 个进程,pcb_table[100]将越界写入相邻内存,可能覆盖main()的栈帧或全局变量。更健壮的做法是动态分配:
#define MAX_PROC 100 struct pcb *pcb_table = NULL; void init_pcb_table() { pcb_table = (struct pcb*)malloc(sizeof(struct pcb) * MAX_PROC); if (!pcb_table) { fprintf(stderr, "Failed to allocate PCB table\n"); exit(1); } memset(pcb_table, 0, sizeof(struct pcb) * MAX_PROC); } void cleanup_pcb_table() { free(pcb_table); pcb_table = NULL; }malloc后必须检查返回值,free后置NULL防止野指针。实验中若init_pcb_table()被调用两次,第二次malloc会覆盖原指针,导致首次分配的内存泄漏——这是pcb.c编译通过但运行崩溃的高频原因。
4. 避坑:天津理工大学实验报告一中 4 个高频翻车点与血泪修复方案
4.1 现象:编译pcb.c报错undefined reference to 'main'
原因:pcb.c是模块文件,不含main()函数,不能直接gcc pcb.c -o pcb。它需与主程序(如main.c)一起编译,或作为库被链接。
解决:确认实验包中是否存在main.c。若有,执行gcc main.c pcb.c -o os_exp1;若只有pcb.c,则需自行编写最小main():
// main.c #include "pcb.h" // 假设头文件名 int main() { init_pcb_table(); create_process(); // 假设此函数在 pcb.c 中定义 return 0; }然后gcc main.c pcb.c -o os_exp1。
4.2 现象:ps aux | grep your_process查不到子进程,或显示<defunct>
原因:父进程未调用waitpid()回收子进程,子进程退出后成为僵尸进程(Zombie),其 PCB 仍驻留内核,但ps显示为Z状态。
解决:在父进程fork()后必须waitpid()。若需创建多个子进程,用循环:
pid_t pids[10]; for (int i = 0; i < 5; i++) { pids[i] = fork(); if (pids[i] == 0) { // 子进程逻辑 exit(0); } } for (int i = 0; i < 5; i++) { waitpid(pids[i], NULL, 0); // 逐个回收 }4.3 现象:子进程输出与父进程混杂,顺序混乱(如Child1ParentChild2)
原因:printf()是行缓冲,stdout未刷新即fork(),导致父子进程共享同一缓冲区内容,fork()后各自输出副本。
解决:在fork()前调用fflush(stdout)强制刷新,或在printf()后加\n触发行缓冲:
printf("Parent about to fork\n"); // 自动刷新 fflush(stdout); // 确保缓冲区清空 pid_t pid = fork();4.4 现象:./os_exp1运行后终端卡死,Ctrl+C无效
原因:子进程进入无限循环(如while(1) sleep(1);),且父进程未设置超时waitpid()或未发送终止信号。
解决:父进程增加超时控制:
struct timespec timeout = { .tv_sec = 5, .tv_nsec = 0 }; int ret = ppoll(NULL, 0, &timeout, NULL); // 等待5秒 if (ret == 0) { printf("Timeout! Killing child...\n"); kill(child, SIGTERM); waitpid(child, NULL, 0); }5. 验证与可观测性:用 /proc 和 strace 看透你的 PCB 在内核中如何存活
5.1 从 /proc/[pid]/stat 解析 PCB 关键字段的映射关系
Linux 将每个进程的 PCB 快照暴露在/proc/[pid]/stat中,共 52 个空格分隔字段。实验中可编写脚本提取关键信息,验证pcb.c设计是否与内核一致:
# 获取当前 shell 的 pid PID=$$ # 读取 stat 文件第 3 字段(state)、第 4 字段(ppid)、第 14 字段(utime)、第 15 字段(stime) awk '{print "State:", $3, "PPID:", $4, "User time:", $14, "System time:", $15}' /proc/$PID/stat输出类似State: S PPID: 1234 User time: 123 System time: 45。其中S表示可中断睡眠(对应BLOCKED),R表示运行(RUNNING),Z表示僵尸。ppid字段值应与pcb.c中parent_pid一致;utime和stime是进程用户态/内核态消耗的 jiffies(1/100 秒),可用于验证sleep(1)是否真让进程让出 CPU——若utime增长缓慢而stime不变,说明进程确实在睡眠。
5.2 用 strace 跟踪 fork()、waitpid()、kill() 的系统调用路径
strace是观测进程与内核交互的黑匣子。对实验程序运行strace -f -e trace=fork,waitpid,kill,exit_group ./os_exp1,输出如下:
2345 fork() = 2346 2345 waitpid(2346, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0) = 2346 2346 exit_group(0) = ?- 第一列
2345是父进程 PID,2346是子进程 PID; waitpid(2346, ...)明确显示父进程等待特定子进程;exit_group(0)是exit()的底层系统调用,证明子进程正确终止。
若waitpid()返回-1,strace会显示waitpid(2346, 0x7ffd1234, 0) = -1 ECHILD (No child processes),说明子进程已提前退出且未被回收,此时需检查fork()后是否有遗漏的waitpid()。
5.3 进程树可视化:用 pstree 验证 parent_pid 的链路完整性
pstree -p $USER以树形展示当前用户所有进程及其 PID。若pcb.c正确设置了parent_pid,你的实验进程应清晰挂载在bash或ssh进程下:
sshd(1234)───bash(1235)───os_exp1(5678)───os_exp1(5679)其中5679是5678的子进程,5678的ppid应为1235。若5679直接挂在systemd下,说明parent_pid未正确继承,fork()后未赋值pcb_table[i].parent_pid = getppid()。
注意:
getppid()返回的是调用时刻的父进程 PID,fork()后子进程需立即调用getppid()获取父 PID 并存入 PCB,而非在父进程中计算后传参——因为fork()后父子进程地址空间独立,参数传递无法保证原子性。
6. 进阶技巧:把 PCB 打印成 JSON 并注入 Prometheus 实现进程指标监控
6.1 用 cJSON 库序列化 PCB 为 JSON,暴露 HTTP 接口
教学实验常止步于终端打印,但真实系统需要可观测性。用轻量级cJSON库将 PCB 数组转为 JSON,再用libmicrohttpd启动 HTTP 服务:
#include <cjson/cJSON.h> #include <microhttpd.h> char* pcb_to_json() { cJSON *root = cJSON_CreateObject(); cJSON_AddNumberToObject(root, "total_processes", current_proc_count); cJSON *procs = cJSON_AddArrayToObject(root, "processes"); for (int i = 0; i < MAX_PROC; i++) { if (pcb_table[i].pid != 0) { cJSON *proc = cJSON_CreateObject(); cJSON_AddNumberToObject(proc, "pid", pcb_table[i].pid); cJSON_AddNumberToObject(proc, "state", pcb_table[i].state); cJSON_AddNumberToObject(proc, "priority", pcb_table[i].priority); cJSON_AddItemToArray(procs, proc); } } char *json_str = cJSON_PrintUnformatted(root); cJSON_Delete(root); return json_str; } int answer_to_connection(void *cls, struct MHD_Connection *connection, const char *url, const char *method, const char *version, const char *upload_data, size_t *upload_data_size, void **ptr) { const char *json = pcb_to_json(); struct MHD_Response *response = MHD_create_response_from_buffer( strlen(json), (void*)json, MHD_RESPMEM_MUST_FREE); MHD_add_response_header(response, "Content-Type", "application/json"); int ret = MHD_queue_response(connection, MHD_HTTP_OK, response); MHD_destroy_response(response); return ret; }编译命令:gcc main.c pcb.c -lcjson -lmicrohttpd -o os_exp1_monitor。运行后访问http://localhost:8888即得实时 PCB JSON 数据。
6.2 用 Prometheus + Grafana 构建进程健康看板
将上述 HTTP 接口注册为 Prometheus target,在prometheus.yml中添加:
scrape_configs: - job_name: 'os_exp1' static_configs: - targets: ['localhost:8888'] metrics_path: '/'编写 exporter 解析 JSON 并暴露指标(Python 示例):
from prometheus_client import Gauge, start_http_server import requests import time proc_count = Gauge('os_exp1_process_count', 'Total number of processes') proc_state = Gauge('os_exp1_process_state', 'Process state (0=READY,1=RUNNING,2=BLOCKED,3=TERMINATED)', ['pid']) def collect_metrics(): try: resp = requests.get('http://localhost:8888') data = resp.json() proc_count.set(data['total_processes']) for proc in data['processes']: proc_state.labels(pid=str(proc['pid'])).set(proc['state']) except: pass if __name__ == '__main__': start_http_server(8000) while True: collect_metrics() time.sleep(1)启动后,Prometheus 可抓取os_exp1_process_count和os_exp1_process_state指标,Grafana 中创建面板显示进程总数趋势、各状态进程分布饼图——这不再是“实验报告”,而是真实运维场景的最小原型。
我带过三届天津理工的操作系统实验课,最深的教训是:别急着交报告,先strace你的fork(),再cat /proc/[pid]/stat看一眼state字段。很多同学卡在“程序没报错但行为不对”,其实只是waitpid()少写了一个括号,或是printf缺了\n导致缓冲区未刷。这些细节不是刁难,而是操作系统在教你——进程不是代码里的对象,它是内核内存中一块有温度、有状态、有生死的真实存在。希望帮到你。
本文还有配套的精品资源,点击获取