
简介本资源是天津理工大学操作系统课程的实验一完整实现包面向计算机专业本科生及操作系统初学者聚焦进程调度核心机制的理解与编程实践。实验基于优先权调度算法通过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, param)强制设置实时策略就能观察到高优进程几乎独占 CPUwhile(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 0return仅退出当前函数而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()子进程将变成僵尸进程Zombieps 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运行后终端卡死CtrlC无效原因子进程进入无限循环如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表示可中断睡眠对应BLOCKEDR表示运行RUNNINGZ表示僵尸。ppid字段值应与pcb.c中parent_pid一致utime和stime是进程用户态/内核态消耗的 jiffies1/100 秒可用于验证sleep(1)是否真让进程让出 CPU——若utime增长缓慢而stime不变说明进程确实在睡眠。5.2 用 strace 跟踪 fork()、waitpid()、kill() 的系统调用路径strace是观测进程与内核交互的黑匣子。对实验程序运行strace -f -e tracefork,waitpid,kill,exit_group ./os_exp1输出如下2345 fork() 2346 2345 waitpid(2346, [{WIFEXITED(s) WEXITSTATUS(s) 0}], 0) 2346 2346 exit_group(0) ?第一列2345是父进程 PID2346是子进程 PIDwaitpid(2346, ...)明确显示父进程等待特定子进程exit_group(0)是exit()的底层系统调用证明子进程正确终止。若waitpid()返回-1strace会显示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()返回的是调用时刻的父进程 PIDfork()后子进程需立即调用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 (0READY,1RUNNING,2BLOCKED,3TERMINATED), [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(pidstr(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导致缓冲区未刷。这些细节不是刁难而是操作系统在教你——进程不是代码里的对象它是内核内存中一块有温度、有状态、有生死的真实存在。希望帮到你。本文还有配套的精品资源点击获取