
简介这份资源是西安电子科技大学操作系统课程的上机实验报告面向正在学习操作系统、需要完成Linux环境下进程与线程编程实验的高校学生。报告围绕进程管理、线程同步与进程间通信展开完整记录了进程的建立、线程共享进程数据、信号通信、匿名管道与命名管道通信以及用信号量实现进程同步等六个实验每个实验均包含实验目的、软硬件环境、程序代码与分析、运行截图和心得体会便于读者对照复现与理解fork、waitpid、pthread_create、signal、sem_init等核心函数的用法。资源包共1个doc文档约377KB结构完整、内容详实适合作为实验参考与复习材料。目前已有1350人学习下载可帮助读者快速掌握Linux下C语言实现进程创建、线程数据共享与通信同步的编程思路为后续并发编程与系统级开发打下基础。1. 六份实验代码摆一起操作系统课设真正该盯的是哪几行操作系统实验这门课很多人交完报告就忘等到面试被问“fork 之后父进程和子进程谁先跑”“管道读端关了会怎样”才发现当年那份报告里全是截图没有一行能复用的判断。我前后翻过好几份操作系统上机报告最值得拿出来讲的是这一份六个实验从进程建立、线程共享数据、信号通信、匿名管道、命名管道到信号量同步代码全在心得也写了但真正有价值的是那几个容易被忽略的细节——waitpid的返回值、fork之后文件描述符的继承、semop的SEM_UNDO标志、命名管道open的阻塞语义。这篇不按实验报告的顺序复述而是按“哪些代码能直接抄、哪些坑我踩过、哪些判断依据面试和课设都用得上”来拆。如果你正在做操作系统上机、准备课设答辩或者想把这几个系统调用真正用熟下面每一段都对应一个能跑的场景。2. 进程与线程fork、waitpid、pthread_create 三行代码背后的判断依据2.1 fork 之后到底谁先跑别信“子进程一定后执行”实验一的代码结构很典型fork()之后用fpid分流子进程读文件父进程waitpid(0,NULL,0)等子进程结束。很多人写报告时默认“父进程先打印、子进程后打印”但实际跑起来顺序是不确定的调度器说了算。这份代码里父进程先打印“我要等待子进程”再调waitpid看起来顺序固定但那只是因为父进程恰好先被调度。真正保证“子进程读完文件父进程才继续”的不是打印顺序而是waitpid这个阻塞调用。判断依据在这里waitpid(0,NULL,0)的第一个参数0表示等待“同进程组的任意子进程”第二个参数传NULL表示不关心退出状态第三个参数0表示阻塞等待。如果你要精确等待某一个子进程第一个参数应该传具体的pid如果你想拿子进程的退出码做判断第二个参数得传int status的地址再用WIFEXITED(status)和WEXITSTATUS(status)去解析。这份报告里传了NULL说明作者只关心“子进程结束了”这个事实不关心它怎么结束的——课设够用但生产代码里这是黑匣子子进程崩了你都不知道。还有一个容易被忽略的点子进程里fopen打开文件后没有fclose靠exit(1)退出时由内核回收。这在实验里没问题但如果子进程不是exit而是从main返回或者父进程在子进程退出前又fork了一次文件描述符的引用计数就会出问题。我一般会建议在fgets循环结束后显式fclose(fp)哪怕后面马上exit养成习惯比省一行代码重要。2.2 线程共享数据sleep(3) 不是同步是赌命实验二的代码更短全局变量shdatapthread_create起一个线程线程里shdata主线程sleep(3)后打印。报告心得写“理解了线程共享数据”但代码里那个sleep(3)暴露了一个典型误区——用睡眠代替同步。线程里只做了一次自增和两次打印3 秒足够跑完所以结果看起来“正确”。但如果线程函数里有个耗时操作或者机器负载高主线程可能在shdata之前就打印了旧值这时候报告里的“共享数据变了”就不成立了。正确的判断依据是线程共享进程的地址空间shdata是全局变量所有线程看到的是同一块内存这一点没错但“一个线程改了另一个线程能立刻看到”需要同步机制保证可见性和顺序sleep只是碰巧。课设里这么写能过但如果你在答辩时被问“如果去掉 sleep 会怎样”答“可能打印 0”比答“没问题”更能体现你懂。我一般会在这个实验里补一句把sleep(3)换成pthread_join(tid,NULL)主线程等线程结束再打印结果才是确定的。pthread_join的第二个参数可以接线程返回值这份代码里线程返回(void*)0传NULL忽略即可。另外注意pthread_create的返回值判断代码里pth!0就报错返回这是对的因为pthread_create成功返回 0失败返回错误码不是设置errno。这一点和很多系统调用不一样写报告时如果写成“检查 errno”就是错的。2.3 编译命令里的隐藏条件-pthread 不是可选项实验二用了pthread_create但报告里没写编译命令。实际编译时如果只写gcc test.c -o test链接阶段会报undefined reference to pthread_create。正确写法是gcc test.c -o test -pthread注意是-pthread而不是-lpthread前者会同时设置编译和链接阶段的宏与库后者只加链接库在某些平台上会影响_REENTRANT宏的定义。这个细节课设报告里经常漏但答辩时老师一问“你怎么编译的”就露馅。我一般会把编译命令写进报告的“实验环境”里gcc -pthread一行比事后解释省事。3. 进程间通信四种方式信号、匿名管道、命名管道的代码逐段拆3.1 信号通信kill 发信号之前signal 必须先注册实验三的代码有一个顺序问题值得说子进程kill(getppid(), SIGALRM)在父进程signal(SIGALRM, ouch)之前就可能执行。fork之后父子进程谁先跑不确定如果子进程先跑完kill此时父进程还没注册处理函数SIGALRM的默认动作是终止进程父进程直接没了后面的while(!alarm_fired)根本不会执行。这份代码能跑通大概率是因为父进程先被调度到了signal那一行但这是运气不是设计。稳妥的写法是在fork之前就signal(SIGALRM, ouch)让处理函数在父子分流之前就位。这样无论谁先跑信号都有处理函数接住。判断依据是信号的注册是进程级的fork之后子进程继承父进程的信号处理设置所以在fork前注册子进程也有一份虽然子进程不发信号给自己但至少父进程这边不会漏。还有一个细节ouch处理函数里只改了alarm_fired这个static int没有做任何打印。这是对的信号处理函数里能安全调用的函数很少printf不在保证可重入的列表里严格说在信号处理函数里调printf是未定义行为。课设里这么写常见但如果你在报告里加一句“处理函数只置标志位打印放在主循环”老师会知道你查过资料。while(!alarm_fired)这个忙等循环也是同理实际代码里应该用pause()或sigsuspend()替代但课设演示够用。3.2 匿名管道先关哪一端决定了会不会死锁实验四的管道代码是六个实验里最值得逐行看的。pipe(fd)创建管道fd[0]读端、fd[1]写端fork之后子进程关写端、父进程关读端然后父写子读。这个“各关一端”的动作是管道通信的核心管道是单向的如果父进程不关读端、子进程不关写端读端在写端没关时会一直阻塞等数据写端在缓冲区满时会阻塞等读端消费两边都不关就是死锁的温床。代码里子进程先sleep(3)再read父进程write之后立刻close(fd[1])。这里有个隐含保证父进程关掉写端后子进程的read在读完数据后会返回 0EOF不会永久阻塞。如果父进程不关写端子进程read完 “hello the world” 之后还会继续等因为管道里可能还有数据。报告里没提这一点但这是判断“管道通信写对了没有”的关键依据写端关闭是读端收到 EOF 的前提。另外注意read(fd[0],readbuf,sizeof(readbuf))没有检查返回值。read返回实际读到的字节数返回 -1 是出错返回 0 是 EOF。这份代码里父进程写入了 15 个字节strlen(hello the world)子进程缓冲区MAX_LINE是 80一次read能读完所以打印正常。但如果写入数据超过缓冲区一次read读不完就需要循环读。课设数据量小看不出问题换成大文件就是翻车现场。我一般会在报告里补一句“read返回值需判断循环读直到返回 0”这是从实验代码到可用代码的分界线。3.3 命名管道mkfifo 的 0666 和 open 的阻塞语义实验五用mkfifo(./fifo,0666)创建命名管道然后fork父进程open只读、子进程open只写。这里有两个容易踩的点。第一mkfifo的第二个参数0666是权限位实际权限还会受umask影响。如果umask是 022创建出来的管道权限是 0644不是 0666。课设里单用户跑没问题但如果换用户跑写端可能因为权限不够打不开。判断依据mkfifo的 mode 参数和open的 mode 一样都是“请求的权限”最终权限是mode ~umask。报告里写 0666 没错但知道 umask 这层的人不多。第二open命名管道的阻塞行为以只读方式open一个命名管道会阻塞直到有进程以只写方式打开它反之亦然。代码里父进程先open(O_RDONLY)子进程后open(O_WRONLY)父进程会阻塞在open上等子进程子进程打开后两边继续。这个顺序能跑通但如果子进程先跑、父进程后跑结果一样因为阻塞是双向等待。真正要注意的是如果只开一端另一端永远不来open就永久阻塞。实验里父子进程都在所以没问题实际用命名管道做两个独立程序通信时一端没起来另一端就卡在open上这是排查“程序没反应”时的第一怀疑对象。代码里子进程sleep(1)再写父进程read之前打印了buf的初始值这个打印在read之前所以输出是空的报告截图里如果看到“read buffer前 ”后面没内容是正常的。read之后打印buf能看到 “HELLO,WORLD”。这里read的返回值ret被判断了0比实验四严谨一点但ret为 0 或 -1 的情况没处理课设够用。3.4 三种通信方式的选型对照把信号、匿名管道、命名管道放一起比判断依据其实很清楚维度信号匿名管道命名管道通信方向单向只能传信号编号单向fd[0] 读 fd[1] 写单向open 时定方向数据载体无数据只有信号类型字节流内核缓冲区字节流文件系统节点适用关系任意进程需权限必须有亲缘关系fork 继承 fd任意进程靠路径名阻塞点处理函数执行时机不确定读空阻塞、写满阻塞open 等待对端、读写同管道典型误用处理函数里调 printf不关写端导致读端等不到 EOF只开一端导致 open 永久阻塞这张表不是让你背是让你在课设报告里写“为什么这个实验用管道不用信号”时有话说信号传不了数据管道能传字节流匿名管道依赖 fork 继承的文件描述符命名管道靠路径名让无亲缘进程也能连上。选型判断就这三行。4. 信号量同步生产者消费者代码里那几行 PV 到底该怎么读4.1 三个信号量的分工full、empty、mutex 各管什么实验六的代码量最大但结构是标准的fullid表示已有产品数初值 0emptyid表示空缓冲区数初值MAXSEM5mutxid是互斥锁初值 1。生产者每生产一个产品先P(emptyid)再P(mutxid)写入后V(mutxid)再V(fullid)。消费者反过来先P(fullid)再P(mutxid)取出后V(mutxid)再V(emptyid)。这个顺序不是随便定的。P(emptyid)在前保证缓冲区有空位才继续P(mutxid)在后保证同一时刻只有一个进程操作共享数组。如果反过来先P(mutxid)再P(emptyid)当缓冲区满时生产者拿着互斥锁等空位消费者想消费却拿不到锁死锁。报告心得里写“刚开始出现了消费者取出生产者还未生产的产品最后查找是因为一个 PV 操作出现了错误”说的就是这类顺序问题——大概率是P(fullid)和P(mutxid)的顺序或者某个V漏了。判断依据信号量的 P 操作是“申请资源不够就阻塞”V 操作是“释放资源唤醒等待者”。fullid和emptyid是计数信号量管的是“有没有产品/有没有空位”mutxid是二元信号量管的是“能不能进临界区”。三者各司其职混用就会出问题。4.2 SEM_UNDO 标志进程异常退出时的后悔药代码里P.sem_flgSEM_UNDO和V.sem_flgSEM_UNDO这个细节值得单独说。SEM_UNDO的作用是如果进程在持有信号量期间异常退出比如段错误、被 kill内核会自动撤销这个进程对该信号量的操作相当于帮你补一个 V。没有这个标志进程崩了之后信号量计数就永久少了一个其他进程可能永远阻塞。课设里进程正常exit加不加SEM_UNDO看不出区别。但如果你在报告里写“考虑了进程异常退出的场景”这就是加分项。我一般会建议所有semop都带上SEM_UNDO代价几乎为零收益是避免一个很难排查的挂死问题。注意SEM_UNDO是每个semop调用独立记录的不是信号量创建时的属性所以 P 和 V 都要写。4.3 共享内存映射mmap 的 MAP_ANONYMOUS 和 fork 继承代码用mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0)创建共享内存array、sum、get、set四个指针都映射到匿名共享内存。这里的关键是MAP_SHAREDfork之后父子进程对这块内存的修改互相可见。如果写成MAP_PRIVATE每个进程有自己的一份拷贝生产者写的array消费者看不到程序逻辑就崩了。MAP_ANONYMOUS表示不关联文件fd传 -1offset传 0。这个用法在 Linux 上没问题但严格说MAP_ANONYMOUS需要_GNU_SOURCE或_DEFAULT_SOURCE宏某些编译环境下不定义会报错。报告里没提编译宏实际编译如果报MAP_ANONYMOUS undeclared在文件开头加#define _GNU_SOURCE即可。这是排查编译错误时的第一反应。还有一个细节sum、get、set三个指针各映射了sizeof(int)array映射了sizeof(int)*MAXSEM。fork之后子进程继承这些映射所以生产者和消费者操作的是同一块物理内存。(*set)和(*get)在mutxid保护下执行不会竞争。如果去掉mutxid两个消费者同时*sum array[...]就会丢更新这是并发编程里最经典的翻车点报告心得里没提但答辩时很可能被问。4.4 消费者退出条件*get 100 的判断位置代码里消费者循环里先P(fullid)再P(mutxid)然后判断if(*get 100) break;。这个判断在P(mutxid)之后、V(mutxid)之前如果 break 了mutxid和fullid都没有 V 操作信号量计数就漏了。课设里两个消费者都 break 后进程退出信号量随进程销毁看不出问题但如果这段代码被复用到一个长期运行的服务里漏掉的 V 会让信号量逐渐失衡。更稳妥的写法是把退出判断放在P(fullid)之前或者 break 之前补上V(mutxid)和V(fullid)。报告里没处理这个边界属于“能跑但不够严谨”。我一般会在报告里加一句“消费者退出前需释放已持有的信号量”这是从实验代码到工程代码的差距所在。5. 避坑排查这六个实验里最容易翻车的五个地方5.1 现象父进程打印“等待子进程”后直接结束子进程输出丢失原因父进程没有调waitpid或者waitpid的第一个参数传错。实验一里如果写成waitpid(-1,NULL,WNOHANG)WNOHANG表示非阻塞子进程还没结束时直接返回 0父进程继续往下走子进程的输出可能还没刷出来父进程就退出了。解决确认waitpid第三个参数是0阻塞第一个参数是0或具体子进程pid。如果要用WNOHANG得配合循环轮询不能调一次就完事。5.2 现象管道实验里子进程 read 一直不返回程序挂住原因父进程写完数据后没有close(fd[1])子进程的read读完已有数据后继续等因为写端还开着内核认为“可能还有数据”。实验四代码里父进程write后立刻close(fd[1])所以能正常返回如果漏了这行就是挂死。解决写端用完必须关读端才能收到 EOFread返回 0。排查时用strace -f ./program看子进程卡在哪个系统调用上如果是read且没有返回先查写端关没关。5.3 现象命名管道程序运行后没有任何输出像卡死原因open命名管道时只开了一端另一端没有进程打开open阻塞。实验五里父子进程都开了所以没问题如果单独跑一个只读程序没有写端open(O_RDONLY)就永久阻塞。解决确认通信双方都启动了或者用O_NONBLOCK打开只读非阻塞打开会立即返回但读不到数据时read返回 -1 并置errno为EAGAIN。排查时ls -l ./fifo看管道文件在不在ps看对端进程起没起。5.4 现象信号量实验里消费者打印的产品编号重复或跳号原因mutxid的 P/V 没有包住整个临界区或者array[(*set)%MAXSEM]的写入和(*set)之间被其他进程插入。实验六代码里P(mutxid)之后写array、(*set)、打印然后V(mutxid)临界区是完整的如果某个 V 漏了或者 P 的顺序错了就会出现重复消费。解决检查每个P(mutxid)是否都有对应的V(mutxid)且成对出现在同一段逻辑里。用ipcs -s看信号量计数如果某个信号量的semval异常比如 mutex 不是 0 或 1说明有进程没释放。5.5 现象编译报undefined reference to pthread_create或MAP_ANONYMOUS undeclared原因前者是没加-pthread编译选项后者是没定义_GNU_SOURCE宏。这两个都是环境配置问题不是代码逻辑问题但课设里经常卡在这一步。解决编译命令统一用gcc test.c -o test -pthread文件开头加#define _GNU_SOURCE放在所有#include之前。如果还报错检查gcc --version和系统头文件路径确认pthread.h和sys/mman.h存在。6. 把这六个实验串成一条线课设答辩前我会重点复述的三段逻辑如果只让我从这份报告里挑三段能在答辩时讲清楚的内容我会选这三段。第一段是fork加waitpid的协同fork创建子进程后父子进程的执行顺序由调度器决定waitpid是父进程主动放弃 CPU 等待子进程结束的手段它的返回值是子进程pid失败返回 -1errno会告诉你原因。这段逻辑能回答“为什么父进程能等到子进程”和“如果子进程先退出父进程还没 wait 会怎样”子进程变僵尸父进程 wait 后回收。第二段是管道的两端关闭匿名管道靠fork继承文件描述符命名管道靠路径名打开两者都是单向字节流写端关闭是读端收到 EOF 的前提读端关闭后写端会收到SIGPIPE信号默认动作是终止进程。这段能解释“管道为什么能当进程同步用”和“写一个已经关闭读端的管道会怎样”。第三段是信号量的 P/V 顺序P(empty)在P(mutex)之前V(mutex)在V(full)之前顺序反了就是死锁或者数据竞争SEM_UNDO是异常退出时的兜底。这段能讲清“生产者消费者为什么需要三个信号量”和“少一个 V 会出什么问题”。这三段逻辑对应的代码都在报告里但报告是按实验顺序排的答辩时按“进程创建—通信—同步”这条线串起来讲比逐个实验念代码更有说服力。我一般会在答辩前把这三段的代码各跑一遍改一个参数看输出变化比如把waitpid的0改成WNOHANG看父进程提前退出把管道的close(fd[1])注释掉看子进程挂住把P(emptyid)和P(mutxid)换个位置看死锁。改一次跑一次比背十遍代码管用。回过头看这份报告最大的价值不是六个实验的截图而是它把 Linux 下进程、线程、通信、同步的主干系统调用都摸了一遍。fork、pthread_create、kill、pipe、mkfifo、semget/semop这些名字在面试里出现的频率不低但真正能说清参数含义和边界条件的人不多。从那以后我每次碰进程间通信的代码都会先把“谁创建、谁等待、谁关闭、谁释放”这四个问题过一遍再动手写。希望这份拆解能帮你在课设和面试里少走几个弯路。本文还有配套的精品资源点击获取