news 2026/10/30 2:54:27

Linux进程通信与同步实验:从fork到信号量的六个实战场景

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux进程通信与同步实验:从fork到信号量的六个实战场景

简介:这份资源是西安电子科技大学操作系统课程的实验报告文档,面向正在学习操作系统原理、需要完成Linux环境下进程与线程编程实验的高校学生。内容围绕进程管理、线程同步与进程间通信展开,涵盖进程的建立、线程共享进程数据、信号通信、匿名管道与命名管道通信,以及用信号量实现进程同步等实验模块,每部分均配有实验目的、软硬件环境说明、完整C语言程序及逐段分析、运行截图与心得体会,可帮助读者对照理解fork()、waitpid()、pthread_create()、sem_wait()等核心函数的用法与协同逻辑。资源包共1个文件,为doc格式实验报告,大小约377KB,结构完整便于直接查阅与参考。目前已有1350人学习下载,适合需要梳理实验流程、复盘代码实现与撰写报告的学习者使用。

1. 六份实验代码摆一起,我才看懂进程与通信的骨架

操作系统这门课,光看教材上的进程状态图,考试能过,但一上机就露馅。我前后把六份实验代码反复跑过、改过、也故意改坏过,最后发现一个反直觉的结论:真正卡住人的从来不是fork()怎么写,而是“谁等谁、谁关哪一端、信号量初值设多少”这三件小事。这份实验报告资源把进程建立、线程共享、信号通信、匿名管道、命名管道、信号量同步六个场景串成一条线,每个场景都配了可编译的 C 代码和函数说明。对刚接触 Linux 系统编程的新手,它是一份能照着敲、照着调错的作业底稿;对已经写过并发程序的熟手,它更像一面镜子,能照出你对父子进程协同、管道半双工、PV 操作这些底层机制到底理解到哪一层。下面我按“怎么跑通、怎么判断对错、哪里最容易翻车”来讲,把这份资源里真正值钱的部分拆开。

2. 进程与线程:从 fork 到 pthread_create 的协同逻辑

2.1 fork 之后父子进程到底谁先跑

实验一的核心不是“创建两个进程”这句话,而是创建之后父子进程的执行顺序完全不确定。代码里父进程调用waitpid(0,NULL,0)阻塞等待,子进程去读文件,这个设计把“协同”落到了实处:父进程不抢跑,子进程干完活父进程才继续。我一般会这么判断一段 fork 代码写得对不对——先看fpid的三个分支有没有漏,<0报错、==0子进程、>0父进程,漏一个就是隐患;再看子进程里exit()有没有调,不调的话子进程可能继续执行父进程的代码路径,输出会乱成一锅粥。

pid_t fpid = fork(); if (fpid < 0) { printf("error"); // 创建失败,必须处理 } else if (fpid == 0) { // 子进程分支:读文件 FILE *fp = fopen("test1file.txt", "r"); if (fp == NULL) { printf("未找到文件\n"); // 文件不存在要给出明确提示 } else { char text[2000]; while (fgets(text, 2000, fp) != NULL) { puts(text); } } exit(1); // 子进程必须显式退出 } else { // 父进程分支:等待子进程结束 int ret = waitpid(0, NULL, 0); printf("父进程:子进程已经结束,ret=%d\n", ret); }

这段代码里waitpid的第一个参数是 0,含义是等待“同一个进程组里的任意子进程”,不是等某个特定 pid。实验场景只有一个子进程,所以写 0 没问题;一旦有多个子进程,就得传具体 pid 或者用-1等任意子进程。status传NULL表示不关心退出状态,想看子进程是正常退出还是被信号杀掉,就得传一个int变量的地址再用WIFEXITED判断。这些细节教材上一笔带过,但调试时全是它们说了算。

2.2 线程共享全局变量,共享的边界在哪

实验二用pthread_create起一个线程,线程里把全局变量shdata加一,主线程sleep(3)之后再读,发现值变了。这个实验想说明的是同一进程内所有线程共享数据段,但很多人跑完只记住了“会变”,没记住“为什么能变、什么时候变”。全局变量在进程的数据段,线程没有独立地址空间,所以线程改的就是主线程看的同一块内存。sleep(3)在这里不是随便写的,它是用来“等线程跑完”的粗糙手段,正规做法应该是pthread_join。

int shdata = 0; // 全局共享数据,位于进程数据段 void *creat(void *arg) { printf("shared data = %d\n", shdata); shdata++; // 直接修改全局变量 printf("after operating: shared data = %d\n", shdata); return (void *)0; } int main() { pthread_t tid; int pth = pthread_create(&tid, NULL, creat, NULL); if (pth != 0) { printf("creat thread error\n"); return -1; } sleep(3); // 粗糙等待,正规应使用 pthread_join printf("shared data = %d\n", shdata); return 0; }

编译时记得加-lpthread,否则链接阶段会报undefined reference to pthread_create,这是新手第一个翻车点。pthread_create的四个参数分别是线程标识符指针、线程属性(NULL表示默认)、线程函数、传给线程函数的参数。实验里参数传NULL,线程函数签名写成void *creat(void *arg)就行。判断这段代码是否合格,我会看两点:线程函数返回值有没有写return (void *)0,主线程有没有用join或至少sleep保证线程跑完再读结果。少了后者,输出顺序就是玄学。

2.3 进程与线程的选型判断依据

把实验一和实验二放一起对比,选型逻辑就清楚了。需要独立地址空间、一个进程崩了不影响另一个,用进程,代价是通信要走管道、信号、共享内存这些机制;需要频繁共享数据、创建销毁开销小,用线程,代价是要自己处理竞态。这份资源里实验一用fork加waitpid解决协同,实验二用全局变量加sleep演示共享,两个场景刚好把“隔离性”和“共享性”的取舍摆出来了。我一般会这么比:先问数据要不要跨进程共享,要就用线程或共享内存;再问容错要求高不高,高就拆进程。实验代码虽然简单,但这个判断框架是能直接搬到实际项目里的。

3. 进程间通信:信号、匿名管道、命名管道怎么选

3.1 信号通信的异步本质与处理函数

实验三让子进程用kill(getppid(), SIGALRM)给父进程发信号,父进程用signal(SIGALRM, ouch)注册处理函数,把alarm_fired置一,主循环靠这个标志位退出。信号是异步的,父进程在while(!alarm_fired)里空转打印no signal,信号一到处理函数打断当前执行流,回来之后循环条件就变了。这里有个容易忽略的点:alarm_fired应该声明成volatile sig_atomic_t,普通int在编译器优化下可能被缓存到寄存器,导致主循环永远看不到变化。

static volatile sig_atomic_t alarm_fired = 0; // 信号处理函数里改的变量要 volatile void ouch(int sig) { alarm_fired = 1; // 只做最简单的赋值,别在信号处理函数里做复杂操作 } int main() { pid_t pid = fork(); if (pid == 0) { kill(getppid(), SIGALRM); // 子进程向父进程发信号 exit(0); } else { signal(SIGALRM, ouch); // 注册处理函数 while (!alarm_fired) { printf("no signal\n"); } printf("I got a signal %d\n", SIGALRM); } return 0; }

信号处理函数里能做的事非常有限,printf严格来说都不该放进去,因为它是非异步信号安全的。实验里把打印放在主循环外面是对的。判断信号代码是否可靠,我会看处理函数是不是只改了标志位、主逻辑是不是在主流程里跑。另外signal在不同系统上语义有差异,更稳的是sigaction,但实验环境用signal足够跑通。

3.2 匿名管道的半双工与端口关闭顺序

实验四用pipe(fd)创建匿名管道,父进程关读端写数据,子进程关写端读数据。管道是半双工的,fd[0]读、fd[1]写,这个方向不能反。最容易翻车的地方是端口没关对:父进程如果不关fd[0],子进程的read可能一直阻塞等不到 EOF;子进程如果不关fd[1],父进程写完之后管道写端还有引用,读端也等不到结束。实验代码里父进程写完关fd[1]、子进程读完关fd[0],这个顺序是对的。

int fd[2]; pipe(fd); // fd[0] 读端,fd[1] 写端 pid_t child_pid = fork(); if (child_pid == 0) { close(fd[1]); // 子进程不用写端,关掉 char readbuf[MAX_LINE]; read(fd[0], readbuf, sizeof(readbuf)); printf("data=%s\n", readbuf); close(fd[0]); exit(0); } else { close(fd[0]); // 父进程不用读端,关掉 char str[] = "hello the world"; write(fd[1], str, strlen(str)); close(fd[1]); // 写完关闭,读端才能收到 EOF }

read的返回值要判断,返回 0 表示对端关闭,返回 -1 表示出错。实验里直接printf读到的缓冲区,没处理读失败的情况,实际用的时候得补上。匿名管道只能用于有亲缘关系的进程,这是它和命名管道最本质的区别,也是选型时的第一道分界线。

3.3 命名管道的文件系统入口与阻塞行为

实验五用mkfifo("./fifo", 0666)在文件系统里创建一个管道文件,然后父子进程分别以只读、只写方式open它。命名管道有名字,所以无亲缘关系的进程也能通过路径打开同一个管道通信。这里的关键判断点是open的阻塞语义:以只读方式打开一个命名管道会阻塞,直到有进程以写方式打开它;反过来也一样。实验里父进程先open读端会卡住,直到子进程open写端,所以代码里子进程sleep(1)再写,顺序上能跑通,但更稳的做法是让写端先开或者用O_NONBLOCK。

int ret = mkfifo("./fifo", 0666); // 创建命名管道,权限 0666 if (ret < 0) { printf("管道创建失败\n"); // 已存在同名文件也会失败,调试时要先删 exit(0); } pid_t fpid = fork(); if (fpid > 0) { int fd = open("./fifo", O_RDONLY); // 只读打开,会阻塞到写端出现 char buf[BUFSIZ] = {0}; read(fd, buf, sizeof(buf)); printf("父进程读到 = %s\n", buf); close(fd); } else { int fd = open("./fifo", O_WRONLY); // 只写打开 char buf[100] = "HELLO,WORLD"; write(fd, buf, strlen(buf)); close(fd); }

mkfifo的第二个参数是权限位,受umask影响,实际权限可能不是 0666。重复运行实验前要先unlink("./fifo")或者手动删掉,否则mkfifo返回 -1,程序直接退出,很多人第一次跑就卡在这。命名管道适合跨独立进程的简单数据传递,但它是半双工、基于字节流的,没有消息边界,传结构体要自己处理粘包。

3.4 三种通信方式的对比结论

维度信号匿名管道命名管道
通信方向单向通知半双工半双工
适用进程关系任意(需 pid)有亲缘关系无亲缘关系
数据量仅信号编号字节流字节流
同步方式异步打断读写阻塞打开与读写均可能阻塞
典型翻车点处理函数做复杂操作端口未关导致阻塞文件已存在导致创建失败

选型时先看进程有没有亲缘关系,没有就排除匿名管道;再看传的是“通知”还是“数据”,只传事件用信号最轻;传数据且进程独立用命名管道。这份资源把三种方式各给了一个可跑的最小例子,价值在于你能在同一台机器上把三种都跑一遍,对比阻塞行为和端口处理,比看十页教材管用。

4. 信号量同步:生产者消费者里的 PV 顺序与初值

4.1 三个信号量的职责划分

实验六是整份资源里最厚的一个,生产者进程生产 100 个产品,两个消费者进程取走并求和,用共享内存放缓冲区,用三个信号量协调:fullid表示已有产品数,初值 0;emptyid表示空缓冲区数,初值MAXSEM;mutxid是互斥锁,初值 1。这三个初值不是随便定的,fullid为 0 意味着消费者一开始就得等,emptyid为 5 意味着生产者最多先放 5 个,mutxid为 1 保证同一时刻只有一个进程动缓冲区。

// 共享内存映射,父子进程看到同一块 int *array = mmap(NULL, sizeof(int) * MAXSEM, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); int *set = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); int *get = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); // 创建并初始化三个信号量 fullid = semget(IPC_PRIVATE, 1, IPC_CREAT | 0666); emptyid = semget(IPC_PRIVATE, 1, IPC_CREAT | 0666); mutxid = semget(IPC_PRIVATE, 1, IPC_CREAT | 0666); union semun arg; arg.val = 0; semctl(fullid, 0, SETVAL, arg); // 初始无产品 arg.val = MAXSEM; semctl(emptyid, 0, SETVAL, arg); // 初始全空 arg.val = 1; semctl(mutxid, 0, SETVAL, arg); // 互斥锁

mmap用MAP_SHARED | MAP_ANONYMOUS是为了让 fork 出来的子进程共享同一块匿名内存,set和get分别是生产指针和消费指针,也要共享,所以单独映射。semget的IPC_PRIVATE表示创建私有信号量集,0666是权限。semctl的SETVAL用来设初值,union semun这个联合体在有些系统上需要自己定义,glibc 头文件里不一定给全,编译报错时补上就行。

4.2 PV 操作的顺序为什么不能换

生产者每生产一个产品,先对emptyid做 P、再对mutxid做 P,放数据,然后对mutxid做 V、对fullid做 V。消费者反过来,先 Pfullid再 Pmutxid,取数据,Vmutxid再 Vemptyid。这个顺序是死规矩:如果生产者先 Pmutxid再 Pemptyid,缓冲区满的时候生产者拿着锁等空位,消费者想取数据却拿不到锁,直接死锁。实验正文里提到“消费者取出生产者还未生产的产品”,根子就是某个 P/V 顺序写反或者漏了。

struct sembuf P = {0, -1, SEM_UNDO}; // P 操作:信号量减一 struct sembuf V = {0, 1, SEM_UNDO}; // V 操作:信号量加一 // 生产者循环体 semop(emptyid, &P, 1); // 等空位 semop(mutxid, &P, 1); // 进临界区 array[(*set) % MAXSEM] = i + 1; // 放产品 (*set)++; semop(mutxid, &V, 1); // 出临界区 semop(fullid, &V, 1); // 产品数加一 // 消费者循环体 semop(fullid, &P, 1); // 等产品 semop(mutxid, &P, 1); // 进临界区 *sum += array[(*get) % MAXSEM]; // 取产品 (*get)++; semop(mutxid, &V, 1); // 出临界区 semop(emptyid, &V, 1); // 空位加一

SEM_UNDO的作用是进程异常退出时自动撤销它做过的 P 操作,避免信号量永久卡死,调试阶段建议保留。semop的第三个参数是操作数组的长度,这里每次只操作一个信号量所以传 1。判断这段同步逻辑对不对,我会盯三处:P 的顺序是不是先资源信号量后互斥信号量、V 的顺序是不是先互斥后资源、set和get的取模是不是都用了MAXSEM。三处都对,生产者消费者就不会出现取空或覆盖。

4.3 共享内存与信号量的配合边界

共享内存负责“数据放哪”,信号量负责“什么时候能放能取”,两者职责不能混。实验里array、sum、get、set全在共享内存,信号量只做计数和互斥。一个常见误用是把互斥锁省掉,觉得fullid和emptyid已经能保证安全,但两个消费者可能同时通过fullid的 P 操作进入临界区,同时改get和sum,结果就是数据错乱。mutxid不能省。另一个边界是MAXSEM设成 5,缓冲区小,生产消费交替频繁,更容易暴露顺序错误,调试时故意把MAXSEM改成 1 或 2,死锁和竞态会更快现形。

5. 避坑与排查:六个实验里最容易翻车的五处

5.1 编译链接报 pthread_create 未定义

现象:实验二代码gcc test.c -o test之后报undefined reference to 'pthread_create'。原因:pthread 库不是默认链接的,需要显式加-lpthread。解决:编译命令改成gcc test.c -o test -lpthread,放在源文件后面,顺序反了在某些链接器上也会失效。

5.2 命名管道重复创建直接退出

现象:实验五第二次运行打印“管道创建失败”然后退出。原因:mkfifo对已存在的同名文件返回 -1,./fifo上次跑完没删。解决:运行前unlink("./fifo"),或者在mkfifo失败时判断errno == EEXIST并继续使用已有管道。调试脚本里加一句rm -f ./fifo最省事。

5.3 匿名管道读端一直阻塞

现象:实验四子进程read不返回,程序挂住。原因:父进程写完没关fd[1],管道写端还有打开的文件描述符,读端等不到 EOF。解决:检查父子进程是否各自关闭了不用的那一端,父进程写完立刻close(fd[1]),子进程读完close(fd[0])。

5.4 信号处理函数改了变量主循环却看不到

现象:实验三父进程一直打印no signal,信号明明发了。原因:alarm_fired是普通int,被编译器优化进寄存器,主循环读的是寄存器旧值。解决:声明成volatile sig_atomic_t,强制每次从内存读。这个坑在-O2优化下必现,-O0下可能侥幸跑通,别被没优化的结果骗了。

5.5 生产者消费者死锁或取到空数据

现象:实验六跑着跑着不动了,或者消费者打印出 0 和未生产的值。原因:P/V 顺序写反,或者mutxid漏做 P,或者fullid、emptyid初值设错。解决:按“先资源后互斥做 P、先互斥后资源做 V”逐行核对,把MAXSEM调小到 1 加速复现,用ipcs -s查看残留信号量,必要时ipcrm清掉再跑。

6. 把六份代码跑成自己的判断力

这份实验报告资源真正的用法,不是把六段代码抄进作业交掉,而是拿它当对照物,逼自己回答几个问题:fork之后父进程为什么必须等、管道两端为什么必须各关一个、信号量初值为什么是 0/5/1 而不是别的数。我的习惯是每跑通一个实验,就故意改坏一处——把waitpid删掉看输出乱序、把close(fd[1])注释掉看读端挂起、把mutxid的 P 操作挪到emptyid前面看死锁——改坏一次再修回来,比顺顺利利跑十遍记得牢。六个实验覆盖了进程创建、线程共享、信号、匿名管道、命名管道、信号量同步,这条线走完,Linux 系统编程里“隔离与共享、同步与通信”的基本盘就有了。资源里的代码可以直接编译,函数说明和心得体会也标出了每个实验的意图,照着跑、照着改,比对着教材空想强得多。需要这份实验报告底稿的,按文中的代码结构和函数清单去取用,先跑通再改坏,最后留下的是你自己的排查手感。从那以后我每次写多进程程序,都会先把端口关闭和 P/V 顺序在纸上过一遍再敲键盘,希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/30 2:53:40

MoE+MLA大模型落地:推理显存、长文本与纯RL训练

简介&#xff1a;一份围绕DeepSeek大模型技术突破的PDF专题资料&#xff0c;面向AI研究者、开发者、投资人及对大模型原理感兴趣的学习者&#xff0c;聚焦MoE架构、MLA机制、强化学习训练路线与R1/V3等标志性成果的解析。内容从诞生背景与幻方量化孵化背景切入&#xff0c;系统…

作者头像 李华
网站建设 2026/10/30 2:53:36

PyTorch张量本质与训练循环底层机制详解

简介&#xff1a;《Deep Learning with PyTorch》是 Manning 出版社推出的经典英文原版教材&#xff0c;面向具备一定 Python 基础、希望系统掌握 PyTorch 与深度学习实践的开发者与学习者&#xff0c;帮助读者从框架基础一路进阶到模型训练、分布式与部署等工程环节。全书围绕…

作者头像 李华
网站建设 2026/10/30 2:51:39

DeepSeek-R1本地部署实战:Ollama一键推理落地指南

简介&#xff1a;本资源是一份面向AI开发者与研究人员的DeepSeek大语言模型本地部署实战指南&#xff0c;聚焦Ollama平台安装、DeepSeek-R1多版本模型下载与运行、Docker容器化管理及Open WebUI图形界面搭建全流程&#xff0c;解决个人设备上低成本、低门槛运行主流开源LLM的核…

作者头像 李华
网站建设 2026/10/30 2:51:08

多帧超分辨率重构实战:亚像素配准、融合与迭代反投影

简介&#xff1a;这份代码与数据包聚焦多帧图像超分辨率重构&#xff0c;面向具备图像处理基础、希望从理论走向MATLAB实现的学习者和开发者&#xff0c;着重解决低分辨率多帧图像融合、恢复与质量评估的落地问题。压缩包共399个文件、约15.38MB&#xff0c;以大量低分辨率与参…

作者头像 李华
网站建设 2026/10/30 2:51:06

发卡系统源码实战:支付回调、卡密池与自动发卡部署

简介&#xff1a;《最新发卡系统网站源码.zip》是一套面向发卡网开发与运营学习者的修复版网站源代码&#xff0c;适合具备PHP基础、希望研究虚拟商品自动售卖流程或进行二次开发的人员使用。压缩包共375个文件&#xff0c;以PHP源码为主&#xff0c;辅以CSS、JavaScript、SQL及…

作者头像 李华
网站建设 2026/10/30 2:50:52

Canon EDSDK 3.6.1 正确安装与 C# 调用避坑指南

简介&#xff1a;本资源是佳能官方EDSDK 3.6.1开发包完整集成版&#xff0c;面向C#、C及VB桌面应用开发者&#xff0c;用于构建相机直连控制类程序&#xff0c;如自动化拍摄系统、远程监拍工具或专业图像采集软件。包内含1599个文件&#xff0c;涵盖119个跨平台DLL&#xff08;…

作者头像 李华