嵌入式 Linux 快速入门(三):进程间通信(IPC)详解
课程摘要:本课程是一份面向嵌入式 Linux 初学者的实践导向速成指南。上一篇我们学习了进程的创建、终止与监控,这一篇我们继续深入进程间通信(IPC)的核心知识。课程摒弃繁杂的理论,以动手实操为核心,带你掌握管道、消息队列、共享内存、信号量等多种进程间通信方式,为后续多进程并发项目开发打下坚实基础。
开始前的准备:跟上一篇一样,VMware + Ubuntu 虚拟机、VSCode + Remote-SSH 插件环境准备好就行,直接开干!
学习目标:
进程间通信(IPC):
理解什么是进程间通信,学会无名管道、有名管道、消息队列、共享内存、信号量的使用,掌握 System V IPC 对象的创建与操作
学习内容:
1. 进程间通信概述:
我们先搞懂两个问题:
什么是进程间通信?
为什么需要进程间通信?
上一篇我们学了,每个进程都有自己独立的地址空间,互相之间是隔离的,一个进程崩了不会影响另一个进程,这是好事。但有时候两个进程需要交换数据、协同工作,这时候就需要进程间通信(IPC,Inter-Process Communication)。
简单说,IPC 就是让两个或多个进程之间能够传递数据、发送信号、同步执行的一套机制。
Linux 下常见的 IPC 方式有这么几种:
| 通信方式 | 特点 | 适用场景 |
|---|---|---|
| 无名管道 | 单向、只能父子/兄弟进程用、简单 | 亲缘进程间简单数据传输 |
| 有名管道 | 单向、任意进程用、有文件路径 | 无亲缘关系进程间通信 |
| 消息队列 | 先进先出、按类型收消息、内核维护 | 结构化消息传递 |
| 共享内存 | 最快、直接读写同一块内存、需同步 | 大量数据高速共享 |
| 信号量 | 计数器、用于互斥和同步 | 保护共享资源、进程同步 |
| Socket | 跨网络、最通用 | 不同机器的进程通信 |
这一篇我们重点学前面五种,Socket 留到下一篇网络编程再讲。
2. 无名管道(pipe)
无名管道是最简单的 IPC 方式,只能在有亲缘关系的进程(比如父子进程、兄弟进程)之间使用,而且是单向的,一端读一端写。
pipe 的函数原型:
#include <unistd.h>
int pipe(int pipefd[2]);
参数说明
| 参数 | 数据类型 | 详细说明 | 使用规则与补充 |
|---|---|---|---|
| pipefd[2] | int [2] | 传出参数,存两个文件描述符 | pipefd[0] 是读端,pipefd[1] 是写端 |
| 返回值 | 含义 |
|---|---|
| 0 | 成功 |
| -1 | 失败,设置 errno |
💡 记住:0 是读,1 是写,就像 0 是 stdin(标准输入),1 是 stdout(标准输出)一样。
使用注意事项:
- 只能用于有亲缘关系的进程间通信(父子、兄弟)
- 管道是单工的,一端读一端写,设计好谁读谁写
- 不用的一端要及时 close 掉,防止资源泄漏
- 管道有固定缓冲区大小(通常 64KB),写满了会阻塞
代码示例(父写子读)
/*
头文件说明:
<stdio.h> printf、perror 标准输入输出
<stdlib.h> exit 函数
<unistd.h> pipe、fork、read、write、close 等系统调用
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
int fd[2]; // 管道两端的文件描述符,fd[0]读,fd[1]写
char buf[100]; // 接收数据的缓冲区
pid_t pid; // fork 返回值
/*
第一步:创建管道
pipe(fd) 会把两个文件描述符填入 fd 数组
失败返回 -1
*/
if (pipe(fd) < 0)
{
perror("pipe 创建失败");
exit(1);
}
/* 第二步:fork 创建子进程 */
pid = fork();
if (pid < 0)
{
perror("fork 失败");
exit(1);
}
else if (pid > 0)
{
// ===== 父进程:负责写 =====
close(fd[0]); // 关闭读端,父进程只写
printf("父进程:pid = %d,准备发消息\n", getpid());
/*
write 向管道写端写数据
参数1:fd[1] 写端文件描述符
参数2:要写的数据
参数3:要写的字节数
*/
write(fd[1], "Hello, 子进程!\n", 100);
close(fd[1]); // 写完关闭写端
}
else
{
// ===== 子进程:负责读 =====
close(fd[1]); // 关闭写端,子进程只读
printf("子进程:pid = %d,准备收消息\n", getpid());
/*
read 从管道读端读数据
参数1:fd[0] 读端文件描述符
参数2:缓冲区地址
参数3:最大读多少字节
*/
read(fd[0], buf, 100);
printf("子进程收到消息:%s\n", buf);
close(fd[0]); // 读完关闭读端
}
return 0;
}
现在来讲讲代码的实现:
1. 管道怎么工作的?
调用 pipe(fd) 之后,内核会在内存里开辟一块缓冲区,然后给你两个文件描述符:
fd[0]:读端,从这里读数据fd[1]:写端,往这里写数据
数据从写端进去,从读端出来,先进先出,就像一根水管一样,所以叫管道。
2. 为什么要 close 掉不用的一端?
因为 fork 之后,父子进程都有这两个文件描述符。如果父进程要写,就要把读端关掉;子进程要读,就要把写端关掉。
这样做有两个好处:
- 避免误操作(比如父进程不小心读了)
- 读端全部关闭时,写端 write 会收到信号;写端全部关闭时,读端 read 会返回 0(读到 EOF)
3. 有名管道(FIFO)
无名管道只能父子进程用,那两个没关系的进程怎么通信?用有名管道,也叫 FIFO(First-In-First-Out)。
有名管道在文件系统里有一个路径名,就像一个特殊的文件,任何进程都可以通过打开这个文件来通信。
mkfifo 的函数原型:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
参数说明
| 参数 | 数据类型 | 详细说明 | 使用规则与补充 |
|---|---|---|---|
| pathname | const char * | 管道文件的路径名 | 比如 “my_fifo” |
| mode | mode_t | 管道文件的权限 | 八进制,比如 0666 表示所有用户可读写 |
| 返回值 | 含义 |
|---|---|
| 0 | 成功 |
| -1 | 失败,设置 errno(文件已存在也会失败) |
注意事项:
- 有名管道可以让非亲缘的两个进程互相通信
- 在文件系统中可见,但内容存在内存里,不占磁盘空间
- 默认阻塞打开:只读打开会等有人写,只写打开会等有人读
代码示例(写端程序)
/*
头文件说明:
<stdio.h> printf、perror
<stdlib.h> exit
<string.h> strlen
<unistd.h> write、close
<fcntl.h> open、O_WRONLY
<sys/types.h> 基本数据类型
<sys/stat.h> mkfifo、文件状态
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
int main()
{
/*
第一步:创建有名管道
参数1:"my_fifo" 管道文件名
参数2:0644 权限(所有者读写,其他人只读)
如果文件已存在,mkfifo 会失败,没关系,继续用就行
*/
mkfifo("my_fifo", 0644);
/* 第二步:只写方式打开管道文件 */
int fd = open("my_fifo", O_WRONLY);
if (fd == -1)
{
perror("打开管道失败");
exit(1);
}
/* 第三步:向管道写入数据 */
char *msg = "Hello, 有名管道!";
write(fd, msg, strlen(msg) + 1); // +1 把字符串结束符也写进去
/* 第四步:关闭 */
close(fd);
printf("写端:消息已发送\n");
return 0;
}
代码示例(读端程序)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
int main()
{
/* 创建管道(已存在就跳过) */
mkfifo("my_fifo", 0644);
/* 只读方式打开管道 */
int fd = open("my_fifo", O_RDONLY);
if (fd == -1)
{
perror("打开管道失败");
exit(1);
}
/* 从管道读取数据 */
char buf[256];
int n = read(fd, buf, 256);
printf("读端收到:%s\n", buf);
close(fd);
return 0;
}
💡 小提示:两个程序要分别编译运行,先运行哪个都行,open 会阻塞等另一边也打开。
4. IPC 对象和 ftok
接下来要学的消息队列、共享内存、信号量,它们都属于 System V IPC 对象。这三个家伙有个共同点:都需要用一个 key(键值) 来标识,就像文件名一样,不同进程通过同一个 key 找到同一个 IPC 对象。
那 key 怎么来的?用 ftok() 函数生成。
ftok 的函数原型:
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
参数说明
| 参数 | 数据类型 | 详细说明 | 使用规则与补充 |
|---|---|---|---|
| pathname | const char * | 一个已存在的文件路径 | 通常用当前目录 “.” |
| proj_id | int | 项目ID,非零整数 | 只用低 8 位,一般填 1、2、3… |
| 返回值 | 含义 |
|---|---|
| 非负整数 | 成功,返回生成的 key |
| -1 | 失败,设置 errno |
💡 记住:相同的 pathname + 相同的 proj_id,一定会生成相同的 key。所以两个进程要用同一个 IPC 对象,就得用一样的参数调用 ftok。
System V IPC 的三板斧操作都是类似的:
- 创建/获取:用 key 创建或打开 IPC 对象
- 操作:读写、加锁解锁等
- 删除:用完删掉,释放内核资源
5. 消息队列
消息队列就是内核里的一个消息链表,进程可以往里面发消息,也可以从里面收消息。消息是有类型的,接收端可以按类型收指定的消息。
消息队列常用函数:
| 步骤 | 函数 | 作用 |
|---|---|---|
| 1 | ftok() | 生成 IPC key |
| 2 | msgget() | 创建或打开消息队列 |
| 3 | msgsnd() | 发送消息 |
| 4 | msgrcv() | 接收消息 |
| 5 | msgctl() | 控制(删除等) |
msgget 的函数原型:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
参数说明
| 参数 | 数据类型 | 详细说明 | 使用规则与补充 |
|---|---|---|---|
| key | key_t | IPC 键值 | ftok 生成的,或 IPC_PRIVATE |
| msgflg | int | 标志位 + 权限 | IPC_CREAT:不存在则创建 IPC_EXCL:已存在则失败 权限位如 0666 |
| 返回值 | 含义 |
|---|---|
| ≥ 0 | 成功,返回消息队列 ID(msgid) |
| -1 | 失败,设置 errno |
msgsnd 的函数原型:
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数说明
| 参数 | 数据类型 | 详细说明 | 使用规则与补充 |
|---|---|---|---|
| msqid | int | 消息队列 ID | msgget 返回的 |
| msgp | const void * | 消息结构体指针 | 必须以 long mtype 开头 |
| msgsz | size_t | 消息正文长度 | 不包括 mtype 的大小 |
| msgflg | int | 控制标志 | 0:阻塞 IPC_NOWAIT:非阻塞 |
消息结构体格式(必须这样定义!):
struct msgbuf {
long mtype; // 消息类型,必须 > 0
char mtext[1024]; // 消息内容,大小自己定
};
msgrcv 的函数原型:
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
参数说明
| 参数 | 数据类型 | 详细说明 | 使用规则与补充 |
|---|---|---|---|
| msqid | int | 消息队列 ID | |
| msgp | void * | 接收缓冲区 | 同样以 long mtype 开头 |
| msgsz | size_t | 正文最大长度 | |
| msgtyp | long | 接收类型 | 0:收第一条(先进先出) >0:收指定类型的第一条 <0:收类型 ≤ |
| msgflg | int | 控制标志 | 0:阻塞 IPC_NOWAIT:非阻塞 MSG_NOERROR:超长则截断 |
msgctl 的函数原型:
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
常用 cmd:
- IPC_RMID:删除消息队列(最重要!用完一定要删)
- IPC_STAT:获取队列状态
- IPC_SET:设置队列属性
代码示例(发送端)
/*
头文件说明:
<stdio.h> printf、perror
<stdlib.h> exit
<string.h> strcpy
<sys/types.h> 基本类型
<sys/ipc.h> ftok
<sys/msg.h> msgget、msgsnd、msgctl
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#define MSG_TYPE 1 // 消息类型
// 消息结构体,必须以 long mtype 开头
struct msgbuf {
long mtype; // 消息类型
char mtext[1024]; // 消息内容
};
int main()
{
key_t key;
int msgid;
struct msgbuf msg;
/* 第一步:生成 key */
key = ftok(".", 1); // 当前目录 + 项目号1
/* 第二步:创建消息队列 */
msgid = msgget(key, IPC_CREAT | 0666);
if (msgid == -1)
{
perror("msgget 失败");
exit(1);
}
/* 第三步:构造消息 */
msg.mtype = MSG_TYPE; // 设置消息类型
strcpy(msg.mtext, "Hello, 这是消息队列发来的消息!");
/* 第四步:发送消息 */
if (msgsnd(msgid, &msg, sizeof(msg.mtext), 0) == -1)
{
perror("msgsnd 失败");
exit(1);
}
printf("发送端:消息已发送\n");
return 0;
}
``
#### 代码示例(接收端)
```c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#define MSG_TYPE 1
struct msgbuf {
long mtype;
char mtext[1024];
};
int main()
{
key_t key;
int msgid;
struct msgbuf msg;
/* 生成 key,和发送端一样 */
key = ftok(".", 1);
/* 打开消息队列 */
msgid = msgget(key, 0666);
if (msgid == -1)
{
perror("msgget 失败");
exit(1);
}
/* 接收指定类型的消息 */
if (msgrcv(msgid, &msg, sizeof(msg.mtext), MSG_TYPE, 0) == -1)
{
perror("msgrcv 失败");
exit(1);
}
printf("接收端收到:%s\n", msg.mtext);
/* 用完删除消息队列 */
if (msgctl(msgid, IPC_RMID, NULL) == -1)
{
perror("msgctl 删除失败");
exit(1);
}
printf("接收端:消息队列已删除\n");
return 0;
}
💡 小技巧:可以用
ipcs -q命令查看系统中的消息队列,ipcrm -q msgid手动删除。
6. 共享内存
消息队列是在内核里的,发消息要从用户态拷到内核态,收消息再从内核态拷到用户态,数据量大了就慢。
共享内存就不一样了——多个进程直接映射同一块物理内存,你写我直接就能看到,不需要拷贝,是最快的 IPC 方式。
但快也有代价:大家都能直接读写,容易出问题,所以共享内存通常要配合信号量来做同步。
共享内存常用函数:
| 步骤 | 函数 | 作用 |
|---|---|---|
| 1 | ftok() | 生成 IPC key |
| 2 | shmget() | 创建共享内存 |
| 3 | shmat() | 映射到进程地址空间 |
| 4 | shmdt() | 解除映射 |
| 5 | shmctl() | 控制(删除等) |
shmget 的函数原型:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
参数说明
| 参数 | 数据类型 | 详细说明 | 使用规则与补充 |
|---|---|---|---|
| key | key_t | IPC 键值 | |
| size | size_t | 共享内存大小(字节) | 创建时指定,通常对齐到页大小 |
| shmflg | int | 标志位 + 权限 | IPC_CREAT:不存在则创建 IPC_EXCL:已存在则失败 权限位如 0666 |
| 返回值 | 含义 |
|---|---|
| ≥ 0 | 成功,返回共享内存 ID(shmid) |
| -1 | 失败,设置 errno |
shmat 的函数原型:
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数说明
| 参数 | 数据类型 | 详细说明 | 使用规则与补充 |
|---|---|---|---|
| shmid | int | 共享内存 ID | |
| shmaddr | const void * | 指定映射地址 | 一般填 NULL,让系统自动选 |
| shmflg | int | 标志位 | 0:读写 SHM_RDONLY:只读 |
| 返回值 | 含义 |
|---|---|
| 非 NULL | 成功,返回映射后的内存指针 |
| (void *)-1 | 失败,设置 errno |
💡 拿到指针之后,就可以像操作普通内存一样读写共享内存了!
shmdt 的函数原型:
int shmdt(const void *shmaddr);
解除当前进程与共享内存的映射,只是断开连接,不会删除共享内存本身。
shmctl 的函数原型:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
常用 cmd:
- IPC_RMID:删除共享内存(标记为待删除,所有进程断开后真正释放)
代码示例(写端)
/*
头文件说明:
<stdio.h> printf、perror
<stdlib.h> exit
<string.h> strncpy
<sys/ipc.h> ftok
<sys/shm.h> shmget、shmat、shmdt、shmctl
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define SHM_SIZE 1024 // 共享内存大小
int main()
{
/* 第一步:生成 key */
key_t key = ftok(".", 1);
if (key == -1)
{
perror("ftok 失败");
exit(1);
}
/* 第二步:创建共享内存 */
int shmid = shmget(key, SHM_SIZE, 0666 | IPC_CREAT);
if (shmid == -1)
{
perror("shmget 失败");
exit(1);
}
/* 第三步:映射到当前进程地址空间 */
char *shmaddr = (char *)shmat(shmid, NULL, 0);
if (shmaddr == (char *)-1)
{
perror("shmat 失败");
exit(1);
}
/* 第四步:写入数据(直接像普通内存一样写) */
strncpy(shmaddr, "Hello, 共享内存!", SHM_SIZE);
printf("写端:数据已写入共享内存\n");
/* 第五步:解除映射 */
if (shmdt(shmaddr) == -1)
{
perror("shmdt 失败");
exit(1);
}
return 0;
}
代码示例(读端)
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define SHM_SIZE 1024
int main()
{
/* 生成 key */
key_t key = ftok(".", 1);
if (key == -1)
{
perror("ftok 失败");
exit(1);
}
/* 获取共享内存 */
int shmid = shmget(key, SHM_SIZE, 0666);
if (shmid == -1)
{
perror("shmget 失败");
exit(1);
}
/* 映射 */
char *shmaddr = (char *)shmat(shmid, NULL, 0);
if (shmaddr == (char *)-1)
{
perror("shmat 失败");
exit(1);
}
/* 直接读取 */
printf("读端收到:%s\n", shmaddr);
/* 解除映射 */
shmdt(shmaddr);
/* 删除共享内存 */
if (shmctl(shmid, IPC_RMID, 0) == -1)
{
perror("shmctl 删除失败");
exit(1);
}
printf("读端:共享内存已删除\n");
return 0;
}
⚠️ 注意:共享内存本身不提供同步机制,如果多个进程同时读写,可能会读到不完整的数据。实际项目中一般配合信号量或互斥锁使用。
7. 信号量
共享内存快是快,但多个进程同时抢着用就乱了。信号量就是用来解决这个问题的——它本质上是一个计数器,用来控制多个进程对共享资源的访问,实现互斥和同步。
最简单的用法:二值信号量(初始值为 1),就像一把锁:
- P 操作(减 1):申请资源,拿到锁才能进
- V 操作(加 1):释放资源,用完把锁还回去
如果信号量值已经是 0 了还做 P 操作,进程就会阻塞等着,直到别人做 V 操作把它唤醒。
信号量常用函数:
| 步骤 | 函数 | 作用 |
|---|---|---|
| 1 | ftok() | 生成 IPC key |
| 2 | semget() | 创建信号量集 |
| 3 | semctl() | 初始化信号量 |
| 4 | semop() | P/V 操作 |
| 5 | semctl() | 删除信号量 |
semget 的函数原型:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
参数说明
| 参数 | 数据类型 | 详细说明 | 使用规则与补充 |
|---|---|---|---|
| key | key_t | IPC 键值 | |
| nsems | int | 信号量集中信号量的个数 | 一般用 1 个就够 |
| semflg | int | 标志位 + 权限 | IPC_CREAT、IPC_EXCL、权限位 |
| 返回值 | 含义 |
|---|---|
| ≥ 0 | 成功,返回信号量集 ID(semid) |
| -1 | 失败,设置 errno |
semctl 的函数原型:
int semctl(int semid, int semnum, int cmd, ... /* union semun arg */);
参数说明
| 参数 | 数据类型 | 详细说明 | 使用规则与补充 |
|---|---|---|---|
| semid | int | 信号量集 ID | |
| semnum | int | 操作第几个信号量(从 0 开始) | |
| cmd | int | 控制命令 | SETVAL:设置值 GETVAL:获取值 IPC_RMID:删除 |
| arg | union semun | 可选参数 | SETVAL 时传 arg.val |
⚠️ 注意:union semun 需要自己定义!有些系统不自带。
// 需要自己定义的联合体
union semun {
int val; // SETVAL 用
struct semid_ds *buf; // IPC_STAT/SET 用
unsigned short *array; // GETALL/SETALL 用
};
semop 的函数原型:
int semop(int semid, struct sembuf *sops, size_t nsops);
sembuf 结构体:
struct sembuf {
unsigned short sem_num; // 操作第几个信号量
short sem_op; // 操作值:-1 是P操作,+1 是V操作
short sem_flg; // 标志:0 阻塞,SEM_UNDO 进程退出自动还原
};
常用模式:
- P 操作(申请资源):
sem_op = -1 - V 操作(释放资源):
sem_op = +1
代码示例
/*
头文件说明:
<stdio.h> printf、perror
<stdlib.h> exit
<unistd.h> fork、sleep
<sys/types.h> 基本类型
<sys/ipc.h> ftok
<sys/sem.h> semget、semctl、semop
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#define SEM_INIT_VAL 1 // 信号量初始值(二值信号量,相当于互斥锁)
// 必须自己定义 semun 联合体!
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
int main()
{
int sem_id;
struct sembuf sem_buf;
/* 第一步:生成 key */
key_t key = ftok(".", 1);
if (key == -1)
{
perror("ftok 失败");
exit(1);
}
/* 第二步:创建信号量集(1个信号量) */
sem_id = semget(key, 1, IPC_CREAT | 0666);
if (sem_id < 0)
{
perror("semget 创建失败");
exit(1);
}
/* 第三步:初始化信号量值为 1 */
union semun arg;
arg.val = SEM_INIT_VAL;
if (semctl(sem_id, 0, SETVAL, arg) < 0)
{
perror("semctl 初始化失败");
exit(1);
}
printf("初始化后信号量值 = %d\n", semctl(sem_id, 0, GETVAL));
/* 第四步:P 操作(申请资源,信号量-1) */
sem_buf.sem_num = 0; // 操作第0个信号量
sem_buf.sem_op = -1; // P操作:减1
sem_buf.sem_flg = SEM_UNDO; // 进程异常退出自动还原
if (semop(sem_id, &sem_buf, 1) < 0)
{
perror("P 操作失败");
exit(1);
}
printf("P 操作后信号量值 = %d\n", semctl(sem_id, 0, GETVAL));
// ===== 这里就是临界区,访问共享资源的地方 =====
printf("===== 我拿到锁了,正在访问共享资源... =====\n");
sleep(2); // 模拟干活
// ============================================
/* 第五步:V 操作(释放资源,信号量+1) */
sem_buf.sem_op = 1; // V操作:加1
if (semop(sem_id, &sem_buf, 1) < 0)
{
perror("V 操作失败");
exit(1);
}
printf("V 操作后信号量值 = %d\n", semctl(sem_id, 0, GETVAL));
/* 第六步:删除信号量 */
if (semctl(sem_id, 0, IPC_RMID, 0) < 0)
{
perror("删除信号量失败");
exit(1);
}
printf("信号量程序执行完毕\n");
return 0;
}
💡 SEM_UNDO 标志很有用:如果进程拿着锁异常退出了,内核会自动帮它 V 回去,避免死锁。
以上就是 Linux 进程间通信的基础入门了,觉得有帮助的友友们可以点赞收藏支持一下小弟!!!
学完了可以自己尝试动手完成一个小练习,步骤如下:
用共享内存 + 信号量实现一个生产者消费者模型:
- 生产者进程:循环往共享内存写数据,写完用信号量通知消费者
- 消费者进程:等待信号量,有数据就读出来打印
- 两个进程通过信号量同步,避免同时访问共享内存
提示:用两个信号量,一个表示"空位数"(初始值为缓冲区大小),一个表示"已用位数"(初始值为 0)。
下面是一个完整的「共享内存 + 信号量实现生产者消费者模型」的 C 语言代码示例,包含生产者进程和消费者进程的完整代码、编译运行步骤以及关键注释。
完整代码示例
1. 头文件与宏定义(common.h)
首先创建一个公共头文件,定义共享内存和信号量的相关参数:
/* common.h - 生产者消费者模型公共定义 */
#ifndef COMMON_H
#define COMMON_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/types.h>
#include <sys/wait.h>
#define BUFFER_SIZE 5 // 缓冲区大小(可存放5个数据项)
#define DATA_SIZE 20 // 每个数据项的大小
#define PROJ_ID 123 // ftok 项目ID
#define SHM_KEY_PATH "." // ftok 路径
// 共享内存数据结构
typedef struct {
char data[BUFFER_SIZE][DATA_SIZE]; // 循环缓冲区
int in; // 生产者写入位置
int out; // 消费者读取位置
} shared_buffer_t;
// 信号量定义
enum {
SEM_EMPTY, // 空槽位信号量(初始值为BUFFER_SIZE)
SEM_FULL, // 已用槽位信号量(初始值为0)
SEM_MUTEX // 互斥信号量(初始值为1,保护in/out指针)
};
// 必须自己定义 semun 联合体
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
// 函数声明
int create_shared_memory(key_t key, size_t size);
void* attach_shared_memory(int shmid);
int create_semaphore_set(key_t key, int nsems);
void init_semaphore(int semid, int semnum, int value);
void semaphore_p(int semid, int semnum);
void semaphore_v(int semid, int semnum);
#endif
2. 公共函数实现(common.c)
/* common.c - 共享内存和信号量工具函数 */
#include "common.h"
// 创建共享内存
int create_shared_memory(key_t key, size_t size) {
int shmid = shmget(key, size, IPC_CREAT | 0666);
if (shmid == -1) {
perror("shmget 失败");
exit(1);
}
return shmid;
}
// 映射共享内存到进程地址空间
void* attach_shared_memory(int shmid) {
void *addr = shmat(shmid, NULL, 0);
if (addr == (void*)-1) {
perror("shmat 失败");
exit(1);
}
return addr;
}
// 创建信号量集
int create_semaphore_set(key_t key, int nsems) {
int semid = semget(key, nsems, IPC_CREAT | 0666);
if (semid == -1) {
perror("semget 失败");
exit(1);
}
return semid;
}
// 初始化信号量值
void init_semaphore(int semid, int semnum, int value) {
union semun arg;
arg.val = value;
if (semctl(semid, semnum, SETVAL, arg) == -1) {
perror("semctl 初始化失败");
exit(1);
}
}
// P操作(申请资源)
void semaphore_p(int semid, int semnum) {
struct sembuf sb;
sb.sem_num = semnum;
sb.sem_op = -1; // 减1
sb.sem_flg = SEM_UNDO; // 进程异常退出时自动还原
if (semop(semid, &sb, 1) == -1) {
perror("semop P操作失败");
exit(1);
}
}
// V操作(释放资源)
void semaphore_v(int semid, int semnum) {
struct sembuf sb;
sb.sem_num = semnum;
sb.sem_op = 1; // 加1
sb.sem_flg = SEM_UNDO;
if (semop(semid, &sb, 1) == -1) {
perror("semop V操作失败");
exit(1);
}
}
3. 生产者进程(producer.c)
/* producer.c - 生产者进程 */
#include "common.h"
int main() {
printf("=== 生产者进程启动 (PID: %d) ===\n", getpid());
// 生成 IPC key
key_t key = ftok(SHM_KEY_PATH, PROJ_ID);
if (key == -1) {
perror("ftok 失败");
exit(1);
}
// 1. 创建共享内存
int shmid = create_shared_memory(key, sizeof(shared_buffer_t));
shared_buffer_t *buffer = (shared_buffer_t*)attach_shared_memory(shmid);
// 初始化缓冲区
buffer->in = 0;
buffer->out = 0;
memset(buffer->data, 0, sizeof(buffer->data));
// 2. 创建信号量集(3个信号量)
int semid = create_semaphore_set(key, 3);
// 3. 初始化信号量(只在第一次创建时执行)
// 注意:实际项目中应该用 IPC_EXCL 判断是否首次创建
init_semaphore(semid, SEM_EMPTY, BUFFER_SIZE); // 初始空槽数 = 缓冲区大小
init_semaphore(semid, SEM_FULL, 0); // 初始已用槽数 = 0
init_semaphore(semid, SEM_MUTEX, 1); // 互斥锁初始为1(可用)
// 4. 生产者循环生产数据
for (int i = 1; i <= 10; i++) { // 生产10个数据项
// 等待空槽位
semaphore_p(semid, SEM_EMPTY);
// 申请互斥锁,保护缓冲区指针
semaphore_p(semid, SEM_MUTEX);
// 生产数据(临界区)
char item[DATA_SIZE];
snprintf(item, DATA_SIZE, "产品-%d", i);
strncpy(buffer->data[buffer->in], item, DATA_SIZE);
printf("生产者: 生产了 %s,放入位置 [%d]\n", item, buffer->in);
// 更新写入位置(循环缓冲区)
buffer->in = (buffer->in + 1) % BUFFER_SIZE;
// 释放互斥锁
semaphore_v(semid, SEM_MUTEX);
// 通知消费者有新数据
semaphore_v(semid, SEM_FULL);
sleep(1); // 模拟生产耗时
}
// 5. 生产结束,发送结束标志
semaphore_p(semid, SEM_EMPTY);
semaphore_p(semid, SEM_MUTEX);
strncpy(buffer->data[buffer->in], "END", DATA_SIZE);
buffer->in = (buffer->in + 1) % BUFFER_SIZE;
semaphore_v(semid, SEM_MUTEX);
semaphore_v(semid, SEM_FULL);
printf("生产者: 生产完成,发送结束标志\n");
// 6. 清理(实际项目中应该等消费者结束后再清理)
sleep(3); // 等待消费者处理完
// 解除共享内存映射
shmdt(buffer);
// 删除共享内存和信号量(由最后一个进程删除)
shmctl(shmid, IPC_RMID, NULL);
semctl(semid, 0, IPC_RMID, 0);
printf("生产者: 进程退出\n");
return 0;
}
4. 消费者进程(consumer.c)
/* consumer.c - 消费者进程 */
#include "common.h"
int main() {
printf("=== 消费者进程启动 (PID: %d) ===\n", getpid());
// 生成 IPC key(必须和生产者相同)
key_t key = ftok(SHM_KEY_PATH, PROJ_ID);
if (key == -1) {
perror("ftok 失败");
exit(1);
}
// 1. 获取共享内存(生产者已创建)
int shmid = shmget(key, sizeof(shared_buffer_t), 0666);
if (shmid == -1) {
perror("shmget 获取失败");
exit(1);
}
shared_buffer_t *buffer = (shared_buffer_t*)attach_shared_memory(shmid);
// 2. 获取信号量集
int semid = semget(key, 3, 0666);
if (semid == -1) {
perror("semget 获取失败");
exit(1);
}
// 3. 消费者循环消费数据
int count = 0;
while (1) {
// 等待有数据可消费
semaphore_p(semid, SEM_FULL);
// 申请互斥锁
semaphore_p(semid, SEM_MUTEX);
// 消费数据(临界区)
char item[DATA_SIZE];
strncpy(item, buffer->data[buffer->out], DATA_SIZE);
// 检查是否为结束标志
if (strcmp(item, "END") == 0) {
printf("消费者: 收到结束标志,停止消费\n");
semaphore_v(semid, SEM_MUTEX);
semaphore_v(semid, SEM_EMPTY);
break;
}
printf("消费者: 消费了 %s,从位置 [%d]\n", item, buffer->out);
// 清空已消费的位置
memset(buffer->data[buffer->out], 0, DATA_SIZE);
// 更新读取位置
buffer->out = (buffer->out + 1) % BUFFER_SIZE;
// 释放互斥锁
semaphore_v(semid, SEM_MUTEX);
// 通知生产者有空槽位了
semaphore_v(semid, SEM_EMPTY);
count++;
sleep(2); // 模拟消费耗时(比生产慢)
}
printf("消费者: 共消费了 %d 个产品\n", count);
// 4. 清理
shmdt(buffer);
printf("消费者: 进程退出\n");
return 0;
}
5. Makefile 编译文件
# Makefile - 编译生产者消费者程序
CC = gcc
CFLAGS = -Wall -g
all: producer consumer
common.o: common.c common.h
$(CC) $(CFLAGS) -c common.c -o common.o
producer: producer.c common.o
$(CC) $(CFLAGS) producer.c common.o -o producer
consumer: consumer.c common.o
$(CC) $(CFLAGS) consumer.c common.o -o consumer
clean:
rm -f producer consumer common.o
.PHONY: all clean
编译与运行步骤
步骤 1:创建文件并编译
# 1. 创建所有源文件
touch common.h common.c producer.c consumer.c Makefile
# 2. 将上面的代码分别复制到对应文件中
# 3. 编译程序
make
# 或者手动编译:
# gcc -c common.c -o common.o
# gcc producer.c common.o -o producer
# gcc consumer.c common.o -o consumer
步骤 2:运行程序(打开两个终端)
终端 1 - 运行生产者:
./producer
终端 2 - 运行消费者:
./consumer
步骤 3:观察输出
生产者输出示例:
=== 生产者进程启动 (PID: 1234) ===
生产者: 生产了 产品-1,放入位置 [0]
生产者: 生产了 产品-2,放入位置 [1]
生产者: 生产了 产品-3,放入位置 [2]
...
生产者: 生产完成,发送结束标志
生产者: 进程退出
消费者输出示例:
=== 消费者进程启动 (PID: 1235) ===
消费者: 消费了 产品-1,从位置 [0]
消费者: 消费了 产品-2,从位置 [1]
...
消费者: 收到结束标志,停止消费
消费者: 共消费了 10 个产品
消费者: 进程退出
关键注释说明
-
三个信号量的作用:
SEM_EMPTY:记录缓冲区中空槽位的数量,初始值为BUFFER_SIZESEM_FULL:记录缓冲区中已用槽位的数量,初始值为0SEM_MUTEX:互斥信号量,保护in和out指针的修改,初始值为1
-
生产者逻辑:
- 先等待空槽位(
P(SEM_EMPTY)) - 再申请互斥锁(
P(SEM_MUTEX)) - 写入数据到
buffer->data[buffer->in] - 更新
in指针(循环缓冲区) - 释放互斥锁(
V(SEM_MUTEX)) - 通知消费者有新数据(
V(SEM_FULL))
- 先等待空槽位(
-
消费者逻辑:
- 先等待有数据(
P(SEM_FULL)) - 再申请互斥锁(
P(SEM_MUTEX)) - 读取
buffer->data[buffer->out] - 更新
out指针 - 释放互斥锁(
V(SEM_MUTEX)) - 通知生产者有空槽位(
V(SEM_EMPTY))
- 先等待有数据(
-
循环缓冲区:
- 使用
(index + 1) % BUFFER_SIZE实现循环 - 避免数组越界,提高缓冲区利用率
- 使用
-
进程同步:
- 生产者不会在缓冲区满时写入
- 消费者不会在缓冲区空时读取
- 通过信号量自动阻塞/唤醒进程
-
资源清理:
- 生产者发送 “END” 标志通知消费者结束
- 消费者收到 “END” 后退出循环
- 最后一个进程负责删除 IPC 资源
扩展练习建议
- 修改缓冲区大小:尝试将
BUFFER_SIZE改为 1(单槽缓冲区)或 10,观察程序行为变化 - 多生产者/多消费者:修改代码支持多个生产者和消费者进程
- 添加随机延迟:在生产/消费时添加随机 sleep 时间,模拟真实场景
- 使用 POSIX 信号量:尝试用
sem_open/sem_wait/sem_post替换 System V 信号量 - 添加统计信息:在共享内存中添加生产/消费计数,两个进程都能看到
通过这个练习,你可以深入理解:
- 共享内存的直接内存访问机制
- 信号量如何实现进程同步
- 生产者-消费者模型的经典实现
- 循环缓冲区的应用
- 多进程编程的资源管理和清理
学习产出:
- 技术笔记 1 遍
- 五种 IPC 方式各写一遍代码
- 共享内存 + 信号量综合练习 1 份
有疑问或是建议的博友们可以评论或私信我,我看到会及时解答或改正的。
更多推荐




所有评论(0)