课程摘要:本课程是一份面向嵌入式 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(标准输出)一样。

使用注意事项:

  1. 只能用于有亲缘关系的进程间通信(父子、兄弟)
  2. 管道是单工的,一端读一端写,设计好谁读谁写
  3. 不用的一端要及时 close 掉,防止资源泄漏
  4. 管道有固定缓冲区大小(通常 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(文件已存在也会失败)

注意事项:

  1. 有名管道可以让非亲缘的两个进程互相通信
  2. 在文件系统中可见,但内容存在内存里,不占磁盘空间
  3. 默认阻塞打开:只读打开会等有人写,只写打开会等有人读
代码示例(写端程序)
/*
头文件说明:
<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 的三板斧操作都是类似的:

  1. 创建/获取:用 key 创建或打开 IPC 对象
  2. 操作:读写、加锁解锁等
  3. 删除:用完删掉,释放内核资源

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 进程间通信的基础入门了,觉得有帮助的友友们可以点赞收藏支持一下小弟!!!

学完了可以自己尝试动手完成一个小练习,步骤如下:

用共享内存 + 信号量实现一个生产者消费者模型:

  1. 生产者进程:循环往共享内存写数据,写完用信号量通知消费者
  2. 消费者进程:等待信号量,有数据就读出来打印
  3. 两个进程通过信号量同步,避免同时访问共享内存

提示:用两个信号量,一个表示"空位数"(初始值为缓冲区大小),一个表示"已用位数"(初始值为 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 个产品
消费者: 进程退出

关键注释说明

  1. 三个信号量的作用

    • SEM_EMPTY:记录缓冲区中空槽位的数量,初始值为 BUFFER_SIZE
    • SEM_FULL:记录缓冲区中已用槽位的数量,初始值为 0
    • SEM_MUTEX:互斥信号量,保护 inout 指针的修改,初始值为 1
  2. 生产者逻辑

    • 先等待空槽位(P(SEM_EMPTY)
    • 再申请互斥锁(P(SEM_MUTEX)
    • 写入数据到 buffer->data[buffer->in]
    • 更新 in 指针(循环缓冲区)
    • 释放互斥锁(V(SEM_MUTEX)
    • 通知消费者有新数据(V(SEM_FULL)
  3. 消费者逻辑

    • 先等待有数据(P(SEM_FULL)
    • 再申请互斥锁(P(SEM_MUTEX)
    • 读取 buffer->data[buffer->out]
    • 更新 out 指针
    • 释放互斥锁(V(SEM_MUTEX)
    • 通知生产者有空槽位(V(SEM_EMPTY)
  4. 循环缓冲区

    • 使用 (index + 1) % BUFFER_SIZE 实现循环
    • 避免数组越界,提高缓冲区利用率
  5. 进程同步

    • 生产者不会在缓冲区满时写入
    • 消费者不会在缓冲区空时读取
    • 通过信号量自动阻塞/唤醒进程
  6. 资源清理

    • 生产者发送 “END” 标志通知消费者结束
    • 消费者收到 “END” 后退出循环
    • 最后一个进程负责删除 IPC 资源

扩展练习建议

  1. 修改缓冲区大小:尝试将 BUFFER_SIZE 改为 1(单槽缓冲区)或 10,观察程序行为变化
  2. 多生产者/多消费者:修改代码支持多个生产者和消费者进程
  3. 添加随机延迟:在生产/消费时添加随机 sleep 时间,模拟真实场景
  4. 使用 POSIX 信号量:尝试用 sem_open/sem_wait/sem_post 替换 System V 信号量
  5. 添加统计信息:在共享内存中添加生产/消费计数,两个进程都能看到

通过这个练习,你可以深入理解:

  • 共享内存的直接内存访问机制
  • 信号量如何实现进程同步
  • 生产者-消费者模型的经典实现
  • 循环缓冲区的应用
  • 多进程编程的资源管理和清理

学习产出:

  • 技术笔记 1 遍
  • 五种 IPC 方式各写一遍代码
  • 共享内存 + 信号量综合练习 1 份

有疑问或是建议的博友们可以评论或私信我,我看到会及时解答或改正的。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐