操作系统实验实战:从fork到信号量,六个实验打通并发编程

发布时间:2026/10/10 2:05:05
操作系统实验实战:从fork到信号量,六个实验打通并发编程 简介这份西安电子科技大学操作系统实验报告面向高校计算机专业学生及操作系统课程学习者聚焦Linux环境下进程管理、线程同步与进程间通信三大核心主题帮助读者通过动手编程加深对操作系统原理的理解。资源包内含1个doc文档约377KB完整记录了六个实验的代码、分析与心得体会涵盖fork()创建父子进程、pthread_create()实现线程共享全局数据、信号通信、匿名管道与命名管道通信以及用信号量完成进程同步等内容。文档中配有可直接参考的C语言程序清单与逐段分析并附有实验截图与心得总结便于读者对照复现实验流程、理解waitpid()、sem_wait()等关键函数的用法排查进程协同与临界区互斥中的常见问题。目前已有1346人学习下载适合正在修读操作系统实验课程或希望夯实Linux系统编程基础的学习者参考借鉴。1. 从一份操作系统实验报告说起六个实验到底在练什么很多人第一次翻开操作系统实验指导书看到 fork、pthread_create、pipe、mkfifo、semget 这一串函数名第一反应是“这不就是背 API 吗”。但真正把代码敲进去、编译、运行、看到父子进程交替输出、看到管道里数据被读走、看到生产者消费者在信号量控制下有序推进才会意识到这些实验练的不是函数记忆而是对“并发执行”这件事的直觉。这份实验报告覆盖了进程建立、线程共享数据、信号通信、匿名管道、命名管道、信号量同步六个实验环境是 Linux gcc语言是 C。它适合正在上操作系统课、需要交实验报告的人也适合已经工作但想补一补系统编程底子的开发者。下面我按“能跑起来、能看懂、能排错”的顺序把这六个实验拆一遍。2. 进程与线程fork、waitpid、pthread_create 的实操拆解2.1 fork 之后到底发生了什么实验一的核心是fork()。调用一次返回两次父进程拿到子进程的 pid子进程拿到 0出错返回 -1。这个“一次调用两次返回”是理解进程创建的关键。子进程复制父进程的地址空间、文件描述符表、信号处理方式但 pid 不同、返回值不同。实验里父进程用waitpid(0, NULL, 0)等待子进程读完文件再继续。这里第一个参数 0 表示等待任意子进程第二个参数不关心退出状态第三个参数 0 表示阻塞等待。常见坑是子进程先于父进程结束父进程没 wait子进程变成僵尸进程ps里看到 Z 状态。#include sys/unistd.h #include sys/wait.h #include unistd.h #include stdio.h #include stdlib.h int main() { pid_t fpid fork(); // 创建子进程 int count 0; if (fpid 0) { printf(error); } else if (fpid 0) { // 子进程分支fpid 为 0 printf(子进程我是子进程,id%d开始读取文件\n, getpid()); FILE *fp fopen(test1file.txt, r); if (fp NULL) { printf(未找到文件\n); } else { char text[2000]; while (fgets(text, 2000, fp) ! NULL) { puts(text); // 输出文件内容 } } printf(子进程读取文件结束\n); exit(1); // 子进程退出避免继续执行父进程分支 } else { // 父进程分支fpid 为子进程 pid printf(父进程我是父进程id%d ,我要等待子进程\n, getpid()); int ret waitpid(0, NULL, 0); // 阻塞等待子进程结束 printf(父进程子进程 已经结束ret%d\n, ret); } return 0; }编译命令是gcc fork_demo.c -o fork_demo运行前先建一个test1file.txt随便写几行内容。运行后你会看到子进程先输出文件内容父进程再输出等待结束。如果文件不存在子进程打印“未找到文件”后退出父进程仍然会等到它结束。参数说明waitpid的 pid 参数可以填具体子进程 pid、-1 表示任意子进程、0 表示同进程组的任意子进程。status 传 NULL 表示不关心退出码。options 传 WNOHANG 可以非阻塞轮询但实验里用 0 就够了。2.2 线程共享数据的验证与编译坑实验二用pthread_create创建线程线程函数里修改全局变量shdata主进程 sleep 3 秒后打印发现值变了。这说明线程共享同一进程的地址空间全局变量、堆内存、文件描述符都是共享的只有栈和寄存器是线程私有的。#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); // 等待线程执行完 printf(creat thread success\n); printf(shared data %d \n, shdata); return 0; } }编译要加-lpthreadgcc thread_demo.c -o thread_demo -lpthread。不加这个链接选项会报undefined reference to pthread_create这是新手最常见的翻车点。另外sleep(3)只是让主线程等一会儿不是同步手段线程如果跑得慢主线程可能先打印。严谨做法是用pthread_join(tid, NULL)替代 sleep。参数说明pthread_create第一个参数是线程标识符指针第二个是线程属性NULL 表示默认第三个是线程函数指针第四个是传给线程函数的参数。线程函数返回void *这里返回 0。3. 进程间通信信号、匿名管道、命名管道的落地细节3.1 信号通信的异步本质实验三用kill(getppid(), SIGALRM)让子进程给父进程发信号父进程用signal(SIGALRM, ouch)注册处理函数把alarm_fired置 1主循环退出。信号是异步的父进程在while(!alarm_fired)里空转直到信号到达。#include unistd.h #include sys/types.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; pid fork(); if (pid 0) { perror(fork failed\n); exit(1); } else if (pid 0) { kill(getppid(), SIGALRM); // 子进程向父进程发信号 exit(0); } else { signal(SIGALRM, ouch); // 父进程注册处理函数 while (!alarm_fired) { printf(no signal\n); } if (alarm_fired) printf(\nI got a signal %d\n, SIGALRM); } return 0; }这里有个时序问题如果子进程在父进程调用signal之前就发了信号父进程可能收不到导致死循环。实际写的时候父进程应该先注册信号处理函数再 fork或者用sigaction配合阻塞/解除阻塞来保证时序。实验代码能跑通是因为 fork 之后父进程通常先执行到 signal但不是绝对可靠。参数说明kill的第二个参数是信号编号SIGALRM 默认动作是终止进程这里被自定义处理函数覆盖。signal返回旧的处理函数指针出错返回 SIG_ERR。3.2 匿名管道的读写端关闭规则实验四用pipe(fd)创建匿名管道fd[0] 是读端fd[1] 是写端。父进程关读端写数据子进程关写端读数据。关键规则管道写端全部关闭后读端 read 返回 0读端全部关闭后写端 write 会触发 SIGPIPE。#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]; pid_t child_pid; if (pipe(fd) 0) { printf(管道创建成功\n); child_pid fork(); if (child_pid -1) { printf(子进程创建失败\n); } else if (child_pid 0) { printf(子进程创建成功\n); close(fd[1]); // 子进程关闭写端 sleep(3); // 模拟处理延迟 char readbuf[MAX_LINE]; read(fd[0], readbuf, sizeof(readbuf)); // 从管道读 printf(子进程读取数据成功\n); 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)); // 向管道写 printf(父进程写入数据成功\n); close(fd[1]); // 关闭写端 printf(父进程关闭写端成功\n); return 0; } } else { printf(管道创建失败\n); } }注意read读到的数据不一定以\0结尾printf(%s)可能越界。稳妥做法是读完后手动补\0或者用write的返回值确定长度。实验里readbuf没初始化如果管道里数据刚好填满打印会出乱码。参数说明pipe的参数是长度为 2 的 int 数组成功返回 0。read返回实际读到的字节数0 表示写端已关闭。write返回实际写入字节数管道满时阻塞。3.3 命名管道的文件系统视角实验五用mkfifo(./fifo, 0666)创建命名管道它会在文件系统里留下一个类型为 p 的文件。之后父子进程分别open这个路径子进程以 O_WRONLY 打开写入父进程以 O_RDONLY 打开读取。#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() { int ret mkfifo(./fifo, 0666); // 创建命名管道 if (ret 0) { printf(管道创建失败\n); exit(0); } else { printf(命名管道创建成功\n); } pid_t fpid fork(); if (fpid 0) { printf(创建子进程失败\n); } else if (fpid 0) { // 父进程读 int fd open(./fifo, O_RDONLY); if (fd 0) { printf(读数据出错\n); exit(0); } char buf[BUFSIZ] {0}; printf(父进程read buffer前 %s\r\n, buf); int ret read(fd, buf, sizeof(buf)); if (ret 0) { printf(父进程read buffer后 %s\r\n, buf); } close(fd); } else { // 子进程写 int fd open(./fifo, O_WRONLY); if (fd 0) { printf(写数据出错\n); exit(0); } char buf[100] HELLO,WORLD; sleep(1); printf(子进程写入管道\n); int ret1 write(fd, buf, strlen(buf)); close(fd); } return 0; }命名管道和匿名管道的区别匿名管道只能用于有亲缘关系的进程命名管道通过路径名可以让任意进程打开。open命名管道时O_RDONLY 会阻塞到有写端打开O_WRONLY 会阻塞到有读端打开。如果只开一端进程会卡在 open 上。实验里父子进程都打开后阻塞解除。参数说明mkfifo的 mode 参数 0666 表示所有用户可读写实际权限受 umask 影响。open的 flags 可以加 O_NONBLOCK 避免阻塞但读写行为会变。4. 信号量同步生产者消费者里的 PV 操作与共享内存4.1 三个信号量的职责划分实验六是六个实验里最复杂的。它用semget创建三个信号量fullid 表示已有产品数量初值 0emptyid 表示空余缓冲区数量初值 MAXSEM5mutxid 表示互斥访问缓冲区初值 1。生产者先对 emptyid 和 mutxid 做 P 操作写入产品再对 mutxid 和 fullid 做 V 操作。消费者先对 fullid 和 mutxid 做 P 操作取出产品再对 mutxid 和 emptyid 做 V 操作。#include sys/mman.h #include sys/types.h #include sys/sem.h #include fcntl.h #include unistd.h #include stdio.h #include errno.h #include time.h #include stdlib.h #define MAXSEM 5 union semun { int val; struct semid_ds *buf; unsigned short *array; }; int fullid, emptyid, mutxid; int main() { struct sembuf P, V; union semun arg; int *array, *sum, *get, *set; // 共享内存映射父子进程共享 array/sum/get/set array (int *)mmap(NULL, sizeof(int) * MAXSEM, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); sum (int *)mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); set (int *)mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); get (int *)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); // 初始化 arg.val 0; semctl(fullid, 0, SETVAL, arg); arg.val MAXSEM; semctl(emptyid, 0, SETVAL, arg); arg.val 1; semctl(mutxid, 0, SETVAL, arg); // P 操作sem_op -1V 操作sem_op 1 V.sem_num 0; V.sem_op 1; V.sem_flg SEM_UNDO; P.sem_num 0; P.sem_op -1; P.sem_flg SEM_UNDO; pid_t fpid1 fork(); if (fpid1 0) { // 生产者 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); // 通知有产品 i; } sleep(1); printf(producer is over\n); exit(0); } else { pid_t fpid2 fork(); if (fpid2 0) { // 消费者 A while (1) { semop(fullid, P, 1); semop(mutxid, P, 1); if (*get 100) break; *sum array[(*get) % MAXSEM]; printf(the comsumer 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); sleep(0.5); } printf(consumer A is over\n); exit(0); } else { // 消费者 B逻辑同 A while (1) { semop(fullid, P, 1); semop(mutxid, P, 1); if (*get 100) break; *sum array[(*get) % MAXSEM]; printf(the comsumer B 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); sleep(0.5); } printf(consumer B is over\n); exit(0); } } }编译命令gcc sem_demo.c -o sem_demo。运行后生产者输出 produce 1 到 100两个消费者交替取出并累加最后打印 sum。参数说明semget的 key 用 IPC_PRIVATE 表示创建新信号量nsems 为 1flag 为 IPC_CREAT|0666。semctl的 SETVAL 用 union semun 传初值。semop的 sembuf 结构里 sem_num 是信号量在集合中的下标sem_op 为 -1 是 P1 是 VSEM_UNDO 让进程退出时自动撤销操作。4.2 共享内存映射的父子继承mmap用 MAP_SHARED | MAP_ANONYMOUS 创建匿名共享映射fork 后父子进程看到同一块内存。array 是 5 个 int 的环形缓冲区set 是生产者的写下标get 是消费者的读下标sum 是累加和。这四个指针本身是局部变量但指向的映射区域是共享的所以(*set)在父进程里改子进程能看到。如果不加 MAP_SHAREDfork 后子进程拿到的是写时复制副本修改不会反映到父进程。这是实验六最容易翻车的地方之一。5. 避坑与排查六个实验里最容易翻车的五件事5.1 僵尸进程与孤儿进程现象ps aux看到子进程状态是 Z或者父进程退出后子进程被 init 收养。原因父进程没有调用 wait/waitpid 回收子进程退出状态。解决父进程在 fork 后必须 wait或者用 signal(SIGCHLD, SIG_IGN) 让内核自动回收。实验一里 waitpid 就是干这个的。5.2 管道读写端未关闭导致阻塞现象读端一直阻塞或者写端收到 SIGPIPE 退出。原因管道需要所有写端关闭后读端才返回 0所有读端关闭后写端才收到 EPIPE。解决fork 后立即关闭不需要的那一端父进程关读端、子进程关写端反之亦然。实验四和实验五都遵循这个规则。5.3 信号处理函数里的竞态现象父进程偶尔收不到信号死循环。原因子进程在父进程 signal 注册之前就发了信号。解决先注册信号处理函数再 fork或者用 sigprocmask 阻塞信号注册后再解除阻塞。实验三的代码顺序有隐患实际写的时候要调整。5.4 信号量初值设错导致死锁现象生产者或消费者卡在 semop 上不动。原因fullid 初值不是 0、emptyid 初值不是缓冲区大小、mutxid 初值不是 1。解决初始化后打印三个信号量的值确认或者用 semctl GETVAL 读回来检查。实验六里 fullid0、emptyid5、mutxid1 是标准配置。5.5 共享内存未用 MAP_SHARED现象子进程修改了变量父进程看不到。原因mmap 用了 MAP_PRIVATE或者用了普通 malloc。解决确认 mmap 的 flags 包含 MAP_SHARED匿名映射加 MAP_ANONYMOUS。实验六里四个映射都用了 MAP_SHARED。6. 从能跑到能讲清楚实验报告的验证方法与进阶习惯把六个实验跑通只是第一步实验报告要能讲清楚“为什么这样写”。我一般会做三件事来验证自己真的懂了。第一改参数看行为变化。比如把实验六的 MAXSEM 从 5 改成 1缓冲区变成单槽生产者和消费者会严格交替改成 10生产者可以连续生产 10 个再阻塞。把 sleep(0.5) 去掉观察输出顺序是否还稳定。把 mutxid 的 P/V 操作注释掉看是否出现数据覆盖。这些改动能让你直观感受到信号量的作用。第二用 strace 跟踪系统调用。strace -f ./fork_demo可以看到 fork、wait4、read、write 的调用顺序和返回值。strace -f ./sem_demo能看到 semop 的阻塞和唤醒。这比看代码更直接。第三整理一份函数速查表。下面这张表是我自己常用的函数所属实验关键参数返回值含义fork实验一无父进程返回子 pid子进程返回 0出错 -1waitpid实验一pid0 表示任意子进程返回子 pid出错 -1pthread_create实验二第三个参数是线程函数成功 0失败错误码kill实验三第二个参数是信号编号成功 0失败 -1pipe实验四fd[0] 读fd[1] 写成功 0失败 -1mkfifo实验五mode 受 umask 影响成功 0失败 -1semget实验六IPC_PRIVATE 创建新信号量返回信号量 id失败 -1semop实验六sem_op-1 是 P1 是 V成功 0失败 -1mmap实验六MAP_SHARED 才能共享成功返回映射地址失败 MAP_FAILED最后说一个习惯每次写完涉及 fork 或 pthread 的代码编译时先把-Wall -Wextra加上让编译器把未使用变量、类型不匹配、隐式声明都报出来。链接 pthread 时确认-lpthread在编译命令末尾。运行前先ulimit -a看一眼进程数限制避免 fork 炸弹。这些动作花不了几秒钟但能省掉大量排查时间。从那以后我每次跑并发程序之前都会先确认信号量初值和管道端关闭顺序再开始编译。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询