ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

六个C程序搞定操作系统实验:进程、线程与IPC实战

六个C程序搞定操作系统实验:进程、线程与IPC实战

简介:西安电子科技大学《操作系统》课程实验报告内含六组完整上机实验,重点围绕进程管理、线程同步与进程间通信三大主题展开。实验一通过fork()与waitpid()实现父子进程协同,由子进程读取文件、父进程等待其结束;实验二演示线程如何直接修改全局共享变量;实验三至五逐一演示信号通信、匿名管道与命名管道的数据传输方式;实验六则利用信号量解决共享资源互斥访问问题。每个实验均配有可直接编译运行的C语言源码、关键函数说明、运行截图与独立心得,方便读者在Linux/gcc环境中边读边练,也能作为课程报告格式与撰写思路的参照。资源为单个doc文档,共377KB,适合操作系统课程学习者、考研复习者以及想通过动手实验理解并发编程的开发者。目前已有1350人浏览学习,代码注释清晰,可对照运行验证。

1. 操作系统实验不是玄学:六个C程序把进程、线程、IPC串起来了

操作系统是不是“玄学”,把这几段实验代码编译跑一遍就有答案。这套报告是某高校操作系统课程的上机记录,六个C语言程序把Linux里最常用的进程、线程和进程间通信手段挨个过了一遍:用fork()创建父子进程,用waitpid()等子进程收工,用pthread_create()证明线程活在同一份内存里,接着kill()信号、pipe()匿名管道、mkfifo()命名管道依次登场,最后用信号量把生产者-消费者问题完整落地。适合正在补操作系统实验的在校生,也适合面试前想快速把IPC细节捡起来的开发者。读完之后你能把这六段代码全部编译运行,知道每段代码里的分支为什么这么写,哪些地方容易踩坑,还能顺手改造成一个小工具。

2. 先跑通进程与线程:实验一、二的代码、编译和运行

实验一和实验二是后面所有通信实验的地基。先把“创建一个进程”“创建一个线程”这两个动作看清楚,再去看管道和信号量才不会乱。这里用一个表格先交代六个实验的考点,后面每个实验逐步展开。

实验核心函数考察点
实验一 进程建立fork()、waitpid()父子进程协同与等待
实验二 线程共享数据pthread_create()线程间全局变量共享
实验三 信号通信kill()、signal()异步通知与信号处理函数
实验四 匿名管道pipe()、read()、write()父子进程单向数据流
实验五 命名管道mkfifo()、open()通过FIFO文件跨进程通信
实验六 信号量同步semget()、semop()、mmap()PV操作解决生产者-消费者问题

表格里能看到整份报告的编排逻辑:先进程、再线程,然后从最轻量的信号一路做到最完整的信号量同步。下面先从实验一的fork()看起。

2.1 fork() 一次返回两次:父子分支是怎么分的

fork()是Linux里最容易让人懵的函数,因为它“调用一次、返回两次”。调用完成后,父进程收到的是子进程的PID,子进程收到的是0;失败时返回-1。这套代码把这个分支写得非常直白:

#include <unistd.h> #include <stdio.h> #include <stdlib.h> int main() { pid_t fpid = fork(); // 调用一次 if (fpid < 0) { printf("error\n"); // fork 失败 exit(1); } else if (fpid == 0) { printf("子进程:我是子进程, id=%d\n", getpid()); } else { printf("父进程:我是父进程, id=%d, 创建了子进程 %d\n", getpid(), fpid); } return 0; }

这段代码在fork()之后的if/else if/else,本质上是靠返回值区分两条执行流:子进程从fpid == 0分支继续走,父进程从else分支继续走。注意fork()之后父子进程各有一份独立的count变量拷贝,改其中一个不会影响另一个——这是“进程地址空间隔离”的直接体现,很多人在写多进程程序时会惯性思维以为变量是共享的,这里是最容易翻车的第一课。

编译命令很简单:gcc exp1.c -o exp1,运行后会先打印父进程信息(先打印哪个不固定,取决于内核调度),然后看到子进程的PID比父进程大几位。

2.2 waitpid 收尸:子进程没退出父进程就要等

单独创建一个进程没什么价值,实验一的核心是“父进程等子进程干完活再继续”。这里用waitpid()实现协同:

#include <sys/wait.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main() { pid_t fpid = fork(); if (fpid < 0) { printf("error\n"); } else if (fpid == 0) { printf("子进程:开始读取文件\n"); FILE *fp = fopen("test1file.txt", "r"); if (fp == NULL) { printf("未找到文件\n"); } else { char text[2000]; while (fgets(text, 2000, fp) != NULL) { puts(text); } fclose(fp); } printf("子进程:读取文件结束\n"); exit(1); // 退出码,waitpid 不关心它的值 } else { printf("父进程:我要等待子进程\n"); int ret = waitpid(0, NULL, 0); printf("父进程:子进程已经结束, ret=%d\n", ret); } return 0; }

waitpid(0, NULL, 0)三个参数依次是:等待哪个子进程、状态收集指针、选项标志。这里pid=0表示等待与父进程同组的任意一个子进程,NULL表示不关心退出状态,0表示阻塞等待。如果你想精确等某个子进程,就把第一个参数改成它的PID;如果改成-1,含义变成“等待任意子进程”,等价于老的wait()。

运行前一定要先在工作目录里创建一个文本文件test1file.txt,比如echo "hello os" > test1file.txt。子进程的fopen("test1file.txt", "r")用的是相对路径,找不到就直接打印“未找到文件”然后结束,此时waitpid()一样会返回,但看不到文件内容。

2.3 pthread_create:线程如何共享进程里的全局变量

实验二把视角从进程切到线程。进程之间内存隔离,线程却共享同一份地址空间,这就是它能直接读写全局变量的原因:

#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> int shdata = 0; // 全局共享数据 void *creat(void *arg) { printf("new pthread ....\n"); 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; } else { sleep(3); // 主线程睡3秒,等子线程跑完 printf("creat thread success\n"); printf("shared data = %d\n", shdata); return 0; } }

pthread_create()四个参数:第一个&tid用来接收线程ID;第二个NULL表示默认属性;第三个creat是线程入口函数,签名必须是void *(*)(void *);第四个NULL是传给线程函数的参数。线程函数里对shdata++,主线程sleep(3)之后再打印,看到的已经是1——同一个变量,两条执行流都能改。

“线程共享进程数据”在这里被演示得很清楚:不是靠传参,不是靠IPC,就是直接操作同一块内存。但注意,sleep(3)属于“睡等”,不是同步原语。后面避坑章节会展开讲为什么这个写法在真实并发场景下不可靠,以及正确的替代方案是什么。

2.4 让实验一、二跑起来的编译命令

实验环境以Linux + gcc为基准,Ubuntu或CentOS都行。先装基础工具,再逐个编译:

sudo apt-get install gcc make echo "hello os" > test1file.txt gcc exp1.c -o exp1 gcc exp2.c -o exp2 -lpthread ./exp1 ./exp2

注意实验二必须加-lpthread链接线程库,否则会报undefined reference to pthread_create。实验一的文件读取依赖test1file.txt存在,运行后你能看到“父进程等待”和“子进程读取结束”的先后顺序是固定的——子进程打印完读取结果,父进程才从waitpid()返回。

3. 信号、匿名管道与命名管道:三种IPC的代码链路

进程之间怎么说话?实验三到实验五给出了三种答案:信号是“喊一嗓子”,匿名管道是“父子之间的水管”,命名管道是“文件系统里的一根水管”。三种方式使用场景不同,代码结构也各有各的坑。

3.1 信号通信:kill(getppid(), SIGALRM) 的父子通知链路

信号是最轻量的异步通知机制,不需要建立连接,一个进程“喊一嗓子”,另一个进程收到后自动跳进处理函数。实验三是子进程给父进程发一个SIGALRM:

#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <signal.h> static int alarm_fired = 0; void ouch(int sig) { alarm_fired = 1; // 信号处理函数里只改标志位 } int main() { pid_t pid = fork(); if (pid < 0) { perror("fork failed"); exit(1); } else if (pid == 0) { // 子进程:向父进程发送 SIGALRM kill(getppid(), SIGALRM); exit(0); } else { // 父进程:注册处理函数,然后忙等 signal(SIGALRM, ouch); while (!alarm_fired) { printf("no signal\n"); } printf("\nI got a signal %d\n", SIGALRM); } return 0; }

signal(SIGALRM, ouch)把SIGALRM的处理函数注册成ouch;子进程kill(getppid(), SIGALRM)把信号发给父进程。getppid()拿到的是“创建我的那个进程”,在fork场景里就是父进程的PID。信号到达后,父进程正在执行的while(!alarm_fired)循环被中断,跳进ouch()把标志位改成1,循环条件失效,继续打印“I got a signal”。

这段代码最值得注意的地方是:信号处理函数里只做“置标志”这种简单操作,没有去调用printf这类非异步安全函数。虽然这里打印也没出事,但如果信号来得密集,在信号处理函数里做重活很容易搞坏主流程的数据。更规范的做法是sigaction()注册处理函数,它能精确控制信号掩码和标志位,signal()在glibc下已经是兼容老接口的包装了。

3.2 匿名管道:close(fd[1]) 与 close(fd[0]) 的读写端控制

管道是“一端写、一端读”的字节流。pipe()创建一个长度为2的数组fd[2],fd[0]是读端,fd[1]是写端。父子进程都持有这两个描述符,所以每一端必须主动关掉自己不用的一方,否则数据流会乱:

#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #define MAX_LINE 80 int main() { int fd[2]; if (pipe(fd) == 0) { pid_t child_pid = fork(); if (child_pid == 0) { // 子进程:关掉写端,只留读端 close(fd[1]); sleep(3); char readbuf[MAX_LINE]; read(fd[0], readbuf, sizeof(readbuf)); printf("子进程读取数据: %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]); printf("父进程写入并关闭写端\n"); return 0; } } return 0; }

创建管道后先fork(),管道描述符随之被父子进程各拷贝一份。子进程执行close(fd[1])关闭写端,父进程执行close(fd[0])关闭读端,管道变成“父写子读”的单向通道。子进程sleep(3)是刻意让父进程先完成写入再读,用时间换顺序确定性。

参数上,read(fd[0], readbuf, sizeof(readbuf))最多读80字节,管道里实际只写了13字节,所以read()返回13,打印出来的字符串不带换行。如果父进程不close(fd[1]),情况会变得很微妙:写端仍处于打开状态,read()会一直阻塞等数据,程序变成“卡死”。管道的核心约定是:只有当所有写端都关闭后,读端的read()才会收到EOF返回0,否则它会无限期等下去。这是匿名管道最容易踩的坑。

3.3 命名管道:mkfifo创建的FIFO文件如何跨进程传数据

匿名管道只能用于有亲缘关系的进程,命名管道通过文件系统里的一个FIFO文件破除了这个限制。任何进程只要能打开这个文件,就能读写数据:

#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> int main() { // 创建命名管道,权限 0666 int ret = mkfifo("./fifo", 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}; int ret = read(fd, buf, sizeof(buf)); if (ret > 0) { printf("父进程读到: %s\n", buf); } close(fd); } else { // 子进程:只写打开 int fd = open("./fifo", O_WRONLY); char buf[100] = "HELLO,WORLD"; sleep(1); write(fd, buf, strlen(buf)); close(fd); } return 0; }

mkfifo("./fifo", 0666)的第一个参数是路径,第二个参数是权限位,0666表示所有人可读可写。fork()之后父进程走fpid > 0分支用O_RDONLY打开,子进程走else分支用O_WRONLY打开,父子进程通过这个FIFO文件交换了一段字符串。

注意FIFO的打开行为和普通文件不一样:以只读方式open()一个FIFO时,如果没有写端打开,open()会阻塞在那里等待。这个实验里父进程先执行到open(O_RDONLY),子进程sleep(1)后执行open(O_WRONLY),一读一写配对成功,程序才会继续往下走。这也是为什么第二次运行这个程序会直接输“管道创建失败”——mkfifo()发现./fifo文件已经存在,返回-1且errno为EEXIST。解决办法是运行前先rm -f ./fifo,或者代码里先remove("./fifo")。

4. 信号量同步解析:生产者-消费者的PV操作实现

实验六是全部实验里代码量最大、也最接近真实并发场景的一个:一个生产者进程生产100个产品,两个消费者进程竞争消费。中间用到三个信号量和一大块共享内存。它的核心不是“读写数据”,而是“控制多个进程对同一块资源的访问顺序”。

4.1 三个信号量:fullid、emptyid、mutxid 各自的职责

信号量的本质是一个带阻塞能力的计数器:P操作尝试把计数减1,不够减就挂起;V操作把计数加1并唤醒等待者。实验里创建了三个信号量:

#include <sys/sem.h> #include <sys/mman.h> #include <sys/types.h> #define MAXSEM 5 union semun { int val; struct semid_ds *buf; unsigned short *array; }; int fullid, emptyid, mutxid; struct sembuf P = {0, -1, SEM_UNDO}; // 减1,不够就阻塞 struct sembuf V = {0, 1, SEM_UNDO}; // 加1,唤醒等待者 int main() { 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; // 空位初始5 semctl(emptyid, 0, SETVAL, arg); arg.val = 1; // 互斥信号量初始1 semctl(mutxid, 0, SETVAL, arg); }

semget(IPC_PRIVATE, 1, IPC_CREAT|0666)创建一组信号量,第一参数IPC_PRIVATE表示不关联文件、只在本程序内使用,第二参数是数量1,第三参数是权限。semctl(id, 0, SETVAL, arg)把第0个信号量的值设为arg.val。三个信号量分工明确:fullid记录“已有产品”,初始0;emptyid记录“空缓冲区”,初始5;mutxid是互斥锁,初始1。

这里的SEM_UNDO值得解释一下:它让信号量的加减在进程退出时自动回滚。如果某进程P操作后没来得及V就崩溃了,内核会把它的欠账还回去,避免其他进程永远卡死。对于课程实验它可有可无,在生产代码里它是一道安全网。

4.2 mmap 共享内存:array、sum、set、get 都放在哪

信号量负责“同步”,但真正传数据要靠共享内存。实验用mmap()一次性映射了四块匿名内存:

int *array = mmap(NULL, sizeof(int) * MAXSEM, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); int *sum = mmap(NULL, sizeof(int), 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);

mmap四个关键参数:第一参数NULL表示让内核选地址;第二参数是映射长度,array映射5个int,其他三个各映射1个;第三参数PROT_READ | PROT_WRITE表示可读可写;第四参数MAP_SHARED | MAP_ANONYMOUS最关键——MAP_SHARED保证父子进程写这块内存相互可见,MAP_ANONYMOUS表示不依赖任何文件。最后一个-1和0分别是文件描述符和偏移,匿名映射下都用不到。

这四个指针的角色分别是:array是环形缓冲区,大小5;set是生产者下一次写的位置;get是消费者下一次读的位置;sum用来累加被消费的产品编号。fork()之后子进程自动继承这四块映射,所有进程看到的是同一片物理内存。

4.3 生产100个、两个消费者抢:完整流程走一遍

生产者逻辑是标准的“先P(empty)再P(mutex)”:

// 生产者进程 int i = 0; while (i < 100) { semop(emptyid, &P, 1); // 申请一个空位 semop(mutxid, &P, 1); // 进入临界区 array[(*set) % MAXSEM] = i + 1; // 生产产品 (*set)++; printf("produce %d\n", i + 1); semop(mutxid, &V, 1); // 离开临界区 semop(fullid, &V, 1); // 产品数+1 i++; } printf("producer is over\n"); exit(0);

消费者逻辑是反过来“先P(full)确认有产品,再P(mutex)拿数据”:

// 消费者进程A while (1) { semop(fullid, &P, 1); // 确认有产品可拿 semop(mutxid, &P, 1); // 进入临界区 if (*get == 100) break; *sum += array[(*get) % MAXSEM]; printf("consumer A get number %d\n", array[(*get) % MAXSEM]); (*get)++; if (*get == 100) { printf("the sum is %d\n", *sum); break; } semop(mutxid, &V, 1); // 离开临界区 semop(emptyid, &V, 1); // 空位+1 sleep(0.5); } printf("consumer A is over\n"); exit(0);

消费者B的代码与A完全相同,只是打印文案不同。两个消费者通过get指针竞争取产品,fullid保证只有“有货”时才能进临界区,mutxid保证同时只有一个进程在操作array和get,emptyid保证生产者不会在缓冲区满时继续生产。array[(*get) % MAXSEM]这行用取模实现环形缓冲:缓冲区只有5格,产品有100个,下标满了就绕回0。

“生产一个、消费一个”的完整时序是这样的:生产者先P(empty)把空位从5减到4,再P(mutex)锁门,写入产品后V(mutex)开门,V(full)把产品数从0加到1。消费者这边P(full)发现产品数为1,减到0,P(mutex)进门拿货,V(mutex)出门,V(empty)把空位还回去。如此交替,full和empty加起来恒等于5,多一个产品都存不下。

拿产品累进sum的设计是验证正确性的关键:100个消费者如果每个数字都被恰好消费一次,sum应该等于1+2+...+100 = 5050。如果同步写错了,sum会偏小或者出现重复消费。

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

六段代码核心机制都不复杂,但每一段都藏着能把人卡半小时的细节。这里把最常翻车的五个现象按“现象 → 原因 → 解决”整理出来,跑代码前先看一遍能省不少事。

5.1 命名管道第二次运行就“创建失败”

现象:实验五第一次运行正常,第二次运行直接打印“管道创建失败”并退出,./fifo文件明明还在。

原因:mkfifo("./fifo", 0666)在文件已存在时返回-1,并设置errno为EEXIST。原代码没有做“已存在就跳过”的处理,直接当失败退出了。

解决:运行前先rm -f ./fifo,或者在代码里调用remove("./fifo")清理旧文件。还有一个容易误判的点:父进程open("./fifo", O_RDONLY)看起来像“卡住不动”,其实那是FIFO在等写端打开,是正常行为,不是死锁。

5.2 实验二里 sleep(3) 掩盖了竞态条件

现象:去掉实验二主线程里的sleep(3)后,经常打印shared data = 0,结果不稳定。

原因:pthread_create()只负责“创建线程”,创建完就返回,不代表子线程已经执行到shdata++。主线程和子线程是并发跑的,主线程先打印了旧值。

解决:用pthread_join()代替sleep(),让主线程明确等待子线程结束:

pthread_join(tid, NULL); printf("shared data = %d\n", shdata);

sleep(3)靠时间换顺序,pthread_join()靠同步机制保证“子线程已退出”。后者才是并发编程里正确的等待姿势,前者只是碰运气。

5.3 匿名管道 read() 卡死,程序不退出

现象:实验四的子进程read(fd[0], readbuf, sizeof(readbuf))一直阻塞,整个程序挂住。

原因:管道读端在“写端全部关闭”之前不会返回EOF。如果父进程忘了close(fd[1]),或者关闭动作晚于子进程的read(),读端会一直等数据。原代码里父进程在两个printf之后执行了close(fd[1]),所以没问题;改代码时稍一疏忽就会踩中。

解决:养成习惯——pipe()之后立刻决定哪端不用,fork()完成后马上close()自己不需要的那个端口。检查思路也简单:看read()之前是不是所有写端都关了。

5.4 消费者取完第100个产品后 break,信号量没释放

现象:实验六运行到后期,一个消费者打印完the sum is 5050后退出,另一个消费者卡在semop(fullid, &P, 1)上,整个程序不结束。

原因:消费者在*get == 100时直接break,跳过了后面的semop(mutxid, &V, 1)和semop(emptyid, &V, 1)。虽然SEM_UNDO会在进程退出时还账,但消费者只是break出while,进程还没退出,互斥锁就被它带走了。这时候另一个消费者即使能等到fullid,也进不了临界区。

解决:把信号量释放放到break之前,或者干脆在循环尾部统一释放:

if (*get == 100) { printf("the sum is %d\n", *sum); semop(mutxid, &V, 1); semop(emptyid, &V, 1); break; }

但这样改完还有一个隐患:另一个消费者会永远阻塞在semop(fullid)上,因为总产品数就100个,已经全被拿走。课程实验里可以接受,因为父进程退出后整个信号量集合会失效;如果要写成长时间运行的服务,必须设计“结束标志”或者用消费者计数来决定退出时机。

5.5 实验一头文件写错、文件没预先创建

现象:gcc exp1.c报错找不到sys/unistd.h;跑起来后子进程打印“未找到文件”。

原因:原代码里写的是#include<sys/unistd.h>,这个头文件在Linux下不存在,正确路径是#include <unistd.h>。另外一个原因是test1file.txt没有创建,fopen()拿相对路径找不到就返回NULL。

解决:头文件改成<unistd.h>;运行前先echo "hello os" > test1file.txt。顺带说一句,waitpid(0, NULL, 0)里的0偶尔会被误读成“等0号进程”,实际含义是“等待同进程组的任意子进程”,换-1语义更直观。

6. 二次开发:把实验六改造成命令行参数版的生产者-消费者工具

实验六的代码把产品和缓冲区大小都写死了:MAXSEM=5、生产100个。想拿它做实验或者验证不同参数下的行为,就得改源码重新编译,太麻烦。我一般会先做三件事:把总量做成命令行参数、把信号量创建封装成函数、加一个简单的参数校验。

先封装信号量创建,原来的semget + semctl四行代码在三个信号量上重复了三次,收敛成一个函数:

union semun { int val; struct semid_ds *buf; unsigned short *array; }; int create_sem(int init_val) { int id = semget(IPC_PRIVATE, 1, IPC_CREAT | 0666); union semun arg; arg.val = init_val; if (semctl(id, 0, SETVAL, arg) == -1) { perror("semctl setval error"); exit(1); } return id; }

然后改main()的人口,接收total和bufsize两个参数:

int main(int argc, char *argv[]) { int total = 100; // 默认生产100个 int bufsize = 5; // 默认缓冲区5格 if (argc > 1) total = atoi(argv[1]); if (argc > 2) bufsize = atoi(argv[2]); if (total <= 0 || total > 10000 || bufsize <= 0 || bufsize > 100) { fprintf(stderr, "参数不合法:total 取 1~10000,bufsize 取 1~100\n"); return 1; } fullid = create_sem(0); emptyid = create_sem(bufsize); mutxid = create_sem(1); array = mmap(NULL, sizeof(int) * bufsize, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); // sum / set / get 的映射与原来一致,略 // fork 生产者、消费者,流程保持不变 }

改造后,./exp6 200 10就能让程序生产200个产品、用10格缓冲区运行。整个生产消费逻辑不用动,只把MAXSEM和100换成变量。验证改造是否正确,我习惯用两条路径:一是看终端输出的sum是否等于1+2+...+total的等差数列求和,即total * (total + 1) / 2;二是用strace跟踪信号量操作序列,确认没有进程在semop上异常阻塞:

gcc exp6.c -o exp6 ./exp6 200 10 strace -f -e trace=semop,mmap,fork ./exp6 50 5

从那以后我每次跑这套实验代码,都会先看一眼“有没有进程还挂在semop上”,确认没有僵尸进程再进行下一组实验。命名管道跑第二遍也习惯了先删fifo文件,线程实验一律用pthread_join而不是sleep硬等。这套代码虽然短,但把它改造成自己的小工具再跑通,你对Linux进程间通信的掌握会比看十遍书都扎实。希望这些经验能帮你少走两步弯路,编译、运行、改数,一次过。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进