环形队列设计——基于共享内存的高效进程间通信

环形队列设计------基于共享内存的高效进程间通信


一句话总结:环形队列(Circular Queue / Ring Buffer)是一种固定大小的、首尾相连的 FIFO 数据结构,通过复用已释放的存储空间,避免了线性队列频繁搬移数据的性能损耗,是解决多进程间高效数据交换问题的经典方案。


第一部分:整体架构设计

1.1 生活中的类比

🍜 就像回转寿司店

回转寿司的传送带就是一个"环形队列"------厨师(生产者)把做好的寿司盘放到传送带上,顾客(消费者)从传送带上取走想吃的寿司。传送带不停地循环转动,空出来的位置又可以放新的寿司。如果某段传送带满了(队列满),厨师就得等一等;如果传送带空了(队列空),顾客就得等一等。多个厨师同时放寿司时,需要互相协调(互斥),避免放串了位置。

1.2 设计背景

在多客户端通信场景中,多个客户端需要频繁地向服务器发送消息发布与订阅请求。为了高效处理这些请求,本方案采用以下设计:

层次 技术选型 解决的问题
进程间通信 共享内存 相比管道、消息队列,共享内存是最高效的 IPC 方式,数据零拷贝
数据缓存 环形队列 固定大小、循环复用,避免线性队列频繁搬移数据的开销
同步互斥 信号量 解决多进程并发访问共享内存时的竞争条件

1.3 系统架构总览

flowchart LR subgraph 生产者[&#34;生产者(客户端们)&#34;] C1[Client 1] C2[Client 2] C3[Client N] end subgraph 队列[&#34;共享内存中的环形队列&#34;] RQ[Ring Queue<br/>rd_idx/wr_idx/capacity/blk_size] SM[Shared Memory<br/>Data Zone] end subgraph 同步[&#34;同步机制&#34;] SMX[SEM_MUTEX<br/>互斥锁] SE[SEM_EMPTY<br/>判空信号量] SF[SEM_FULL<br/>判满信号量] end subgraph 消费[&#34;消费者(服务器)&#34;] SV[Server] end C1 -->|写入请求| RQ C2 -->|写入请求| RQ C3 -->|写入请求| RQ RQ -->|读取请求| SV SMX -.->|保护| RQ SE -.->|同步| RQ SF -.->|同步| RQ

1.4 三大模块划分

模块 职责 核心函数 类比
共享内存模块 创建/映射/删除共享内存 shm_acquire, shm_attach, shm_detach, shm_remove 快递柜的柜体
信号量模块 互斥与同步控制 sem_set_create, sem_wait, sem_post, sem_set_remove 快递柜的锁和指示灯
环形队列模块 数据缓存与读写管理 ring_queue_init, ring_queue_put, ring_queue_get, ring_queue_destroy 快递柜的格子分配规则

第二部分:共享内存模块设计

一句话总结:共享内存是 Linux 下最高效的 IPC 方式------多个进程通过映射同一块物理内存,实现数据零拷贝的直接交换,避免了内核态与用户态之间的数据复制开销。

2.1 生活中的类比

🏢 就像公司里的共享白板

共享内存好比公司茶水间的一块公共白板。任何部门的人都可以在上面写字(写入数据),其他人可以随时来看(读取数据)。所有人在同一块白板上操作,不需要把内容抄到自己的笔记本上(零拷贝)。白板本身是物理存在的(共享内存段),而各人看到的"视野"就是映射到各自进程地址空间的视图。用完擦掉白板(删除共享内存),大家就看不到了。

2.2 共享内存示意图

2.3 共享内存操作流程

flowchart LR A[ftok 生成唯一key] --> B[shmget 创建/获取共享内存] B --> C{是否成功} C -->|是| D[shmat 映射到进程地址空间] C -->|否| E[返回错误] D --> F[读写数据] F --> G[shmdt 解除映射] G --> H[shmctl IPC_RMID 删除] H --> I[结束] style A fill:#e1f5fe style B fill:#e1f5fe style D fill:#c8e6c9 style F fill:#fff9c4 style G fill:#ffccbc style H fill:#ffccbc

2.4 共享内存 API 封装

shm_util.h ------ 头文件定义

c 复制代码
#ifndef SHM_UTIL_H
#define SHM_UTIL_H

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>

/* 共享内存获取结果枚举 */
typedef enum {
    SHM_ALREADY_EXISTS = 0,   /* 共享内存已存在,直接复用 */
    SHM_NEWLY_CREATED,        /* 新创建成功 */
    SHM_ACQUIRE_FAILED        /* 获取失败 */
} shm_result_t;

/* 函数声明 */
extern shm_result_t shm_acquire(size_t size, int *out_shm_id);
extern void *shm_attach(int shm_id);
extern int shm_detach(const void *shm_addr);
extern int shm_remove(int shm_id);

#endif

shm_util.c ------ 函数实现

c 复制代码
#include "shm_util.h"

/* ftok 参数:使用当前目录下的文件生成唯一 IPC 键值 */
#define FTOK_PATH   "."
#define PROJECT_ID  101

/*
 * 获取共享内存:先尝试获取已有段,不存在则创建新段
 * 参数:
 *   size       - 共享内存大小(字节)
 *   out_shm_id - 输出参数,返回共享内存 ID
 * 返回:
 *   SHM_ALREADY_EXISTS / SHM_NEWLY_CREATED / SHM_ACQUIRE_FAILED
 */
shm_result_t shm_acquire(size_t size, int *out_shm_id)
{
    key_t ipc_key;
    int shm_id;

    ipc_key = ftok(FTOK_PATH, PROJECT_ID);

    /* 先尝试获取已有的共享内存 */
    shm_id = shmget(ipc_key, size, 0);
    if (shm_id == -1) {
        /* 不存在,创建新的 */
        shm_id = shmget(ipc_key, size, IPC_CREAT | 0644);
        if (shm_id == -1) {
            perror("[ERROR] shmget() failed");
            return SHM_ACQUIRE_FAILED;
        }
        *out_shm_id = shm_id;
        return SHM_NEWLY_CREATED;
    }

    *out_shm_id = shm_id;
    return SHM_ALREADY_EXISTS;
}

/* 将共享内存映射到当前进程的地址空间 */
void *shm_attach(int shm_id)
{
    void *addr = shmat(shm_id, NULL, 0);
    if (addr == (void *)-1) {
        perror("[ERROR] shmat() failed");
        return NULL;
    }
    return addr;
}

/* 解除共享内存映射 */
int shm_detach(const void *shm_addr)
{
    return shmdt(shm_addr);
}

/* 标记共享内存为待删除(所有进程 detach 后实际释放) */
int shm_remove(int shm_id)
{
    return shmctl(shm_id, IPC_RMID, NULL);
}

2.5 完整示例与执行结果

写端程序(shm_write.c)------ 向共享内存写入数据

c 复制代码
#include <stdio.h>
#include <string.h>
#include "shm_util.h"

#define SHM_BUF_SIZE 256

int main(void)
{
    int shm_id;
    shm_result_t result;
    void *addr = NULL;

    /* 获取共享内存 */
    result = shm_acquire(SHM_BUF_SIZE, &shm_id);
    if (result == SHM_NEWLY_CREATED)
        printf(">>> 共享内存创建成功\n");
    else if (result == SHM_ALREADY_EXISTS)
        printf(">>> 共享内存已存在,直接复用\n");
    else {
        printf(">>> 共享内存获取失败\n");
        return -1;
    }

    /* 映射到地址空间 */
    addr = shm_attach(shm_id);
    if (addr == NULL) {
        printf(">>> 共享内存映射失败\n");
        return -1;
    }

    /* 写入数据 */
    memset(addr, 'A', 10);
    printf(">>> 已向共享内存写入 10 个字节 'A'\n");

    /* 解除映射(写端不删除,留给读端读取) */
    shm_detach(addr);
    return 0;
}

读端程序(shm_read.c)------ 从共享内存读取数据

c 复制代码
#include <stdio.h>
#include <string.h>
#include "shm_util.h"

#define SHM_BUF_SIZE 256

int main(void)
{
    int shm_id;
    shm_result_t result;
    void *addr = NULL;
    char buffer[16] = {0};

    /* 获取共享内存 */
    result = shm_acquire(SHM_BUF_SIZE, &shm_id);
    if (result == SHM_ALREADY_EXISTS)
        printf(">>> 获取共享内存成功\n");
    else {
        printf(">>> 共享内存尚未创建,请先运行写端\n");
        return -1;
    }

    /* 映射到地址空间 */
    addr = shm_attach(shm_id);
    if (addr == NULL) {
        printf(">>> 共享内存映射失败\n");
        return -1;
    }

    /* 从共享内存读取数据 */
    memcpy(buffer, addr, 10);
    printf(">>> 读取到的数据: %s\n", buffer);

    /* 解除映射并删除共享内存 */
    shm_detach(addr);
    shm_remove(shm_id);
    printf(">>> 共享内存已删除\n");
    return 0;
}

执行结果:

bash 复制代码
# 终端 1:先运行写端
$ gcc -o shm_write shm_write.c shm_util.c
$ ./shm_write
>>> 共享内存创建成功
>>> 已向共享内存写入 10 个字节 'A'

# 终端 2:再运行读端
$ gcc -o shm_read shm_read.c shm_util.c
$ ./shm_read
>>> 获取共享内存成功
>>> 读取到的数据: AAAAAAAAAA
>>> 共享内存已删除

关键点 :写端和读端通过 ftok 生成的相同 IPC 键值找到同一块共享内存段。写端写入后不删除,读端读取完再清理,确保数据传递完整。

⚠️ 注意事项

  • ftok(FTOK_PATH, PROJECT_ID) 依赖 FTOK_PATH 对应的文件路径,必须在同一目录下运行程序
  • shm_remove() 只是标记待删除,实际物理内存释放发生在所有进程执行 shm_detach 之后

第三部分:环形队列数据结构

一句话总结:环形队列由"头部控制区(header)+ 数据存储区(data_zone)"组成,header 记录读写索引等元信息,data_zone 被划分为多个等长数据块(block),通过取模运算实现指针的循环回绕。

3.1 生活中的类比

🎡 就像摩天轮上的座舱

摩天轮有固定数量的座舱(capacity),每个座舱可容纳固定人数(blk_size)。乘客从入口上车(写入),到出口下车(读取)。摩天轮不停地旋转,入口工作人员(写指针 wr_idx)和出口工作人员(读指针 rd_idx)各自记录当前轮到哪个座舱。当座舱转完一圈回到起点时,取模操作让指针自然回绕,就像摩天轮永不停歇地循环。

3.2 环形队列内存布局

flowchart LR subgraph 共享内存区域 H[queue_header_t<br/>rd_idx/wr_idx/capacity/blk_size/sem_set_id] B0[Block 0] B1[Block 1] B2[Block 2] B3[...] B4[Block N-1] end H --> B0 B0 --> B1 B1 --> B2 B2 --> B3 B3 --> B4 style H fill:#e1f5fe,stroke:#1565c0 style B0 fill:#c8e6c9,stroke:#2e7d32 style B1 fill:#c8e6c9,stroke:#2e7d32 style B2 fill:#c8e6c9,stroke:#2e7d32 style B3 fill:#f5f5f5,stroke:#9e9e9e style B4 fill:#c8e6c9,stroke:#2e7d32

3.3 数据结构定义

环形队列的头部控制区:

c 复制代码
/* 队列头部:记录元信息和同步状态 */
typedef struct queue_header {
    int rd_idx;       /* 读索引(下一个要读取的 block 编号) */
    int wr_idx;       /* 写索引(下一个要写入的 block 编号) */
    int capacity;     /* 环形队列中 block 的总数 */
    int blk_size;     /* 每个 block 的字节大小 */
    int sem_set_id;   /* 信号量集合 ID,用于同步互斥 */
} queue_header_t;

环形队列整体结构:

c 复制代码
/* 环形队列句柄:连接头部和数据区 */
typedef struct ring_queue {
    queue_header_t *header;   /* 指向共享内存中头部区域的指针 */
    char *data_zone;          /* 指向数据存储区的指针(紧跟在 header 之后) */
} ring_queue_t;

3.4 内存布局详解

flowchart LR subgraph 共享内存布局 A[queue_header_t 20字节] B[Block 0 blk_size字节] C[Block 1 blk_size字节] D[Block 2 blk_size字节] E[...] F[Block N-1 blk_size字节] end A --> B B --> C C --> D D --> E E --> F style A fill:#ffcdd2,stroke:#c62828 style B fill:#c8e6c9,stroke:#2e7d32 style C fill:#c8e6c9,stroke:#2e7d32 style D fill:#c8e6c9,stroke:#2e7d32 style E fill:#f5f5f5,stroke:#9e9e9e style F fill:#c8e6c9,stroke:#2e7d32

计算方式

  • 共享内存总大小 total_size = capacity × blk_size + sizeof(queue_header_t)
  • 第 i 个 block 的偏移量 offset = i × blk_size
  • 实际地址 addr = data_zone + offset
  • data_zone = (char *)(header + 1) ------ 指针算术,跳过 header 结构体

第四部分:环形队列初始化与销毁

一句话总结:初始化就是"搭架子"------分配句柄、创建共享内存、映射地址空间、设置初始状态;销毁就是"拆架子"------释放信号量、解除映射、删除共享内存、释放内存。

4.1 生活中的类比

🏪 就像开一家回转寿司店

  • 初始化:租场地(分配数据结构)→ 安装传送带(创建共享内存)→ 打开电源(映射地址空间)→ 设置起始位置(rd_idx=0, wr_idx=0)→ 安装红绿灯(创建信号量)
  • 销毁:关掉红绿灯(删除信号量)→ 断电(解除映射)→ 拆除传送带(删除共享内存)→ 退租(释放内存)

4.2 初始化流程

flowchart TD Start([开始初始化]) --> A[分配 ring_queue_t 结构体] A --> B[计算共享内存总大小<br/>total = capacity * blk_size + sizeof header] B --> C[创建/获取共享内存] C --> D{创建成功?} D -->|失败| Err[返回 NULL] D -->|成功| E[映射共享内存到地址空间] E --> F{是否新创建的?} F -->|是| G[初始化 header 区域<br/>rd_idx=0, wr_idx=0, capacity, blk_size] G --> H[创建信号量集合<br/>SEM_MUTEX, SEM_EMPTY, SEM_FULL] H --> I[计算 data_zone 起始地址<br/>data_zone = header + 1] F -->|否| I I --> Ret([返回 ring_queue_t 指针])

4.3 信号量初始化机制

信号量是环形队列的"交通指挥系统",三个信号量各司其职:

信号量 初始值 作用 类比
SEM_MUTEX(索引 0) 1 互斥锁,确保同一时刻只有一个进程操作共享内存 厕所门锁------一次只能进一个人
SEM_EMPTY(索引 1) 0 判空信号量,表示队列中可读数据的数量 餐盘计数器------每做好一份菜加一
SEM_FULL(索引 2) capacity 判满信号量,表示队列中剩余可写空间的数量 空位计数器------每放一份菜减一
c 复制代码
unsigned short init_values[] = {1, 0, capacity};
queue->header->sem_set_id = sem_set_create(3, init_values);

4.4 初始化完整代码

⚠️ 全局变量说明g_shared_mem_id 保存共享内存 ID,由于创建和销毁在不同函数中完成,设为文件级全局变量便于跨函数访问。

c 复制代码
/* 文件级全局变量:共享内存 ID(在 ring_queue.c 顶部声明) */
static int g_shared_mem_id = -1;

/* 初始化环形队列 */
ring_queue_t *ring_queue_init(int capacity, int blk_size)
{
    int total_size = 0;
    shm_result_t acquire_result;
    unsigned short init_values[] = {1, 0, capacity};

    /* 1. 分配环形队列句柄 */
    ring_queue_t *queue = (ring_queue_t *)malloc(sizeof(ring_queue_t));
    if (!queue) return NULL;

    /* 2. 计算共享内存总大小 */
    total_size = capacity * blk_size + sizeof(queue_header_t);

    /* 3. 创建/获取共享内存 */
    acquire_result = shm_acquire(total_size, &g_shared_mem_id);
    if (acquire_result == SHM_ACQUIRE_FAILED) {
        free(queue);
        return NULL;
    }

    /* 4. 映射共享内存到进程地址空间 */
    queue->header = (queue_header_t *)shm_attach(g_shared_mem_id);
    if (queue->header == NULL) {
        free(queue);
        return NULL;
    }

    /* 5. 如果是新创建的共享内存,初始化 header 区域 */
    if (acquire_result == SHM_NEWLY_CREATED) {
        queue->header->rd_idx    = 0;
        queue->header->wr_idx    = 0;
        queue->header->capacity  = capacity;
        queue->header->blk_size  = blk_size;
        queue->header->sem_set_id = sem_set_create(3, init_values);
    }

    /* 6. 计算 data_zone 起始地址(跳过 header 结构体) */
    queue->data_zone = (char *)(queue->header + 1);

    return queue;
}

4.5 销毁流程

flowchart TD Start([开始销毁]) --> A[删除信号量 sem_set_remove] A --> B[解除共享内存映射 shm_detach] B --> C[删除共享内存 shm_remove] C --> D[释放 ring_queue_t 结构体 free] D --> E([结束])

4.6 销毁完整代码

c 复制代码
/* 销毁环形队列 */
void ring_queue_destroy(ring_queue_t *queue)
{
    if (queue == NULL) return;

    sem_set_remove(queue->header->sem_set_id);  /* 1. 删除信号量集合 */
    shm_detach(queue->header);                   /* 2. 解除共享内存映射 */
    shm_remove(g_shared_mem_id);                 /* 3. 删除共享内存 */
    free(queue);                                 /* 4. 释放句柄 */
}

第五部分:环形队列数据读写实现

一句话总结:写数据时先判满再取锁,写入后移动写指针;读数据时先判空再取锁,读取后移动读指针;通过取模运算实现指针的环形回绕,信号量保证了生产者和消费者的同步协作。

5.1 生活中的类比

🚉 就像地铁站的安检传送带

  • 写入数据:乘客把行李放上传送带(写操作)。如果传送带满了(SEM_FULL=0),乘客必须等待。放行李时,其他乘客不能同时放(SEM_MUTEX 互斥锁)。放好后,告知出口处有新行李到达(SEM_EMPTY+1)。
  • 读取数据:乘客在出口取走行李(读操作)。如果传送带空了(SEM_EMPTY=0),乘客必须等待。取行李时同样需要互斥。取走后,告知入口处有空位了(SEM_FULL+1)。

5.2 写数据流程

flowchart TD Start([写数据请求]) --> A[P操作 SEM_FULL<br/>检查队列是否已满] A --> B{有空间?} B -->|满, 阻塞等待| A B -->|有空间| C[P操作 SEM_MUTEX<br/>获取互斥锁] C --> D[计算写位置<br/>offset = wr_idx * blk_size] D --> E[memcpy 写入数据] E --> F[更新写指针<br/>wr_idx = wr_idx + 1 取模回绕] F --> G[V操作 SEM_MUTEX<br/>释放互斥锁] G --> H[V操作 SEM_EMPTY<br/>通知消费者有新数据] H --> I([写入完成]) style A fill:#ffcdd2 style C fill:#ffcdd2 style E fill:#c8e6c9 style F fill:#c8e6c9 style G fill:#bbdefb style H fill:#bbdefb

5.3 读数据流程

flowchart TD Start([读数据请求]) --> A[P操作 SEM_EMPTY<br/>检查队列是否为空] A --> B{有数据?} B -->|空, 阻塞等待| A B -->|有数据| C[P操作 SEM_MUTEX<br/>获取互斥锁] C --> D[计算读位置<br/>offset = rd_idx * blk_size] D --> E[memcpy 读取数据] E --> F[更新读指针<br/>rd_idx = rd_idx + 1 取模回绕] F --> G[V操作 SEM_MUTEX<br/>释放互斥锁] G --> H[V操作 SEM_FULL<br/>通知生产者有空位] H --> I([读取完成]) style A fill:#ffcdd2 style C fill:#ffcdd2 style E fill:#c8e6c9 style F fill:#c8e6c9 style G fill:#bbdefb style H fill:#bbdefb

5.4 写数据实现

c 复制代码
/* 信号量索引 */
#define SEM_MUTEX  0   /* 互斥锁 */
#define SEM_EMPTY  1   /* 判空信号量 */
#define SEM_FULL   2   /* 判满信号量 */

/* 向环形队列写入一个数据块 */
void ring_queue_put(ring_queue_t *queue, const void *data)
{
    int offset = 0;

    /* 1. P操作:判断队列是否已满(满则阻塞等待消费者取走数据) */
    sem_wait(queue->header->sem_set_id, SEM_FULL);

    /* 2. P操作:获取互斥锁(防止多个进程同时写入) */
    sem_wait(queue->header->sem_set_id, SEM_MUTEX);

    /* 3. 计算当前写位置的字节偏移量 */
    offset = queue->header->wr_idx * queue->header->blk_size;

    /* 4. 将数据拷贝到环形队列中 */
    memcpy(queue->data_zone + offset, data, queue->header->blk_size);

    /* 5. 更新写指针(取模运算实现环形回绕) */
    queue->header->wr_idx = (queue->header->wr_idx + 1) % queue->header->capacity;

    /* 6. V操作:释放互斥锁 */
    sem_post(queue->header->sem_set_id, SEM_MUTEX);

    /* 7. V操作:通知消费者有新数据可读 */
    sem_post(queue->header->sem_set_id, SEM_EMPTY);
}

5.5 读数据实现

c 复制代码
/* 从环形队列读取一个数据块 */
void ring_queue_get(ring_queue_t *queue, void *buf)
{
    int offset = 0;

    /* 1. P操作:判断队列是否为空(空则阻塞等待生产者写入数据) */
    sem_wait(queue->header->sem_set_id, SEM_EMPTY);

    /* 2. P操作:获取互斥锁 */
    sem_wait(queue->header->sem_set_id, SEM_MUTEX);

    /* 3. 计算当前读位置的字节偏移量 */
    offset = queue->header->rd_idx * queue->header->blk_size;

    /* 4. 从环形队列中读取数据 */
    memcpy(buf, queue->data_zone + offset, queue->header->blk_size);

    /* 5. 更新读指针(取模运算实现环形回绕) */
    queue->header->rd_idx = (queue->header->rd_idx + 1) % queue->header->capacity;

    /* 6. V操作:释放互斥锁 */
    sem_post(queue->header->sem_set_id, SEM_MUTEX);

    /* 7. V操作:通知生产者有空位可写入 */
    sem_post(queue->header->sem_set_id, SEM_FULL);
}

5.6 环形队列工作状态演示

flowchart LR subgraph 初始状态 I0[Block 0 空] I1[Block 1 空] I2[Block 2 空] I0 --> I1 --> I2 RP[rd_idx=0] -.-> I0 WP[wr_idx=0] -.-> I0 end subgraph 写入1个数据后 W0[Block 0 数据A] W1[Block 1 空] W2[Block 2 空] W0 --> W1 --> W2 RP2[rd_idx=0] -.-> W0 WP2[wr_idx=1] -.-> W1 end subgraph 写入满3个数据 F0[Block 0 数据A] F1[Block 1 数据B] F2[Block 2 数据C] F0 --> F1 --> F2 RP3[rd_idx=0] -.-> F0 WP3[wr_idx=0] -.-> F0 end subgraph 读取1个数据后 R0[Block 0 已读] R1[Block 1 数据B] R2[Block 2 数据C] R0 --> R1 --> R2 RP4[rd_idx=1] -.-> R1 WP4[wr_idx=0] -.-> R0 end subgraph 写入1个数据后 W0[Block 0<br/>数据A] W1[Block 1<br/>空] W2[Block 2<br/>空] W0 --> W1 --> W2 RP2[rd_idx=0] -.-> W0 WP2[wr_idx=1] -.-> W1 end subgraph 写入满3个数据 F0[Block 0<br/>数据A] F1[Block 1<br/>数据B] F2[Block 2<br/>数据C] F0 --> F1 --> F2 RP3[rd_idx=0] -.-> F0 WP3[wr_idx=0] -.-> F0 end subgraph 读取1个数据后 R0[Block 0<br/>已读] R1[Block 1<br/>数据B] R2[Block 2<br/>数据C] R0 --> R1 --> R2 RP4[rd_idx=1] -.-> R1 WP4[wr_idx=0] -.-> R0 end style RP fill:#ff5252,stroke:#d50000 style WP fill:#448aff,stroke:#2962ff style RP2 fill:#ff5252,stroke:#d50000 style WP2 fill:#448aff,stroke:#2962ff style RP3 fill:#ff5252,stroke:#d50000 style WP3 fill:#448aff,stroke:#2962ff style RP4 fill:#ff5252,stroke:#d50000 style WP4 fill:#448aff,stroke:#2962ff

第六部分:生产者-消费者完整示例

一句话总结:通过 fork 创建父子进程,父进程作为消费者读取数据,子进程作为生产者写入数据,环形队列配合信号量机制完美实现了多进程间的同步协作。

6.1 ring_queue.c ------ 环形队列完整实现

c 复制代码
/* ring_queue.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "ring_queue.h"
#include "sem_util.h"

/* 文件级全局变量:共享内存 ID */
static int g_shared_mem_id = -1;

/* 初始化环形队列 */
ring_queue_t *ring_queue_init(int capacity, int blk_size)
{
    int total_size = 0;
    shm_result_t acquire_result;
    unsigned short init_values[] = {1, 0, capacity};

    ring_queue_t *queue = (ring_queue_t *)malloc(sizeof(ring_queue_t));
    if (!queue) return NULL;

    total_size = capacity * blk_size + sizeof(queue_header_t);
    acquire_result = shm_acquire(total_size, &g_shared_mem_id);

    if (acquire_result == SHM_ACQUIRE_FAILED) {
        free(queue);
        return NULL;
    }

    queue->header = (queue_header_t *)shm_attach(g_shared_mem_id);
    if (queue->header == NULL) {
        free(queue);
        return NULL;
    }

    if (acquire_result == SHM_NEWLY_CREATED) {
        queue->header->rd_idx    = 0;
        queue->header->wr_idx    = 0;
        queue->header->capacity  = capacity;
        queue->header->blk_size  = blk_size;
        queue->header->sem_set_id = sem_set_create(3, init_values);
    }

    queue->data_zone = (char *)(queue->header + 1);
    return queue;
}

/* 写入数据 */
void ring_queue_put(ring_queue_t *queue, const void *data)
{
    int offset = 0;

    sem_wait(queue->header->sem_set_id, SEM_FULL);
    sem_wait(queue->header->sem_set_id, SEM_MUTEX);

    offset = queue->header->wr_idx * queue->header->blk_size;
    memcpy(queue->data_zone + offset, data, queue->header->blk_size);
    queue->header->wr_idx = (queue->header->wr_idx + 1) % queue->header->capacity;

    sem_post(queue->header->sem_set_id, SEM_MUTEX);
    sem_post(queue->header->sem_set_id, SEM_EMPTY);
}

/* 读取数据 */
void ring_queue_get(ring_queue_t *queue, void *buf)
{
    int offset = 0;

    sem_wait(queue->header->sem_set_id, SEM_EMPTY);
    sem_wait(queue->header->sem_set_id, SEM_MUTEX);

    offset = queue->header->rd_idx * queue->header->blk_size;
    memcpy(buf, queue->data_zone + offset, queue->header->blk_size);
    queue->header->rd_idx = (queue->header->rd_idx + 1) % queue->header->capacity;

    sem_post(queue->header->sem_set_id, SEM_MUTEX);
    sem_post(queue->header->sem_set_id, SEM_FULL);
}

/* 销毁环形队列 */
void ring_queue_destroy(ring_queue_t *queue)
{
    if (queue == NULL) return;

    sem_set_remove(queue->header->sem_set_id);
    shm_detach(queue->header);
    shm_remove(g_shared_mem_id);
    free(queue);
}

6.2 完整代码

ring_queue.h ------ 环形队列模块头文件

c 复制代码
#ifndef RING_QUEUE_H
#define RING_QUEUE_H

#include "shm_util.h"

/* 信号量索引 */
#define SEM_MUTEX  0
#define SEM_EMPTY  1
#define SEM_FULL   2

/* 环形队列头部控制结构 */
typedef struct queue_header {
    int rd_idx;       /* 读索引 */
    int wr_idx;       /* 写索引 */
    int capacity;     /* block 总数 */
    int blk_size;     /* 每个 block 大小 */
    int sem_set_id;   /* 信号量集合 ID */
} queue_header_t;

/* 环形队列句柄 */
typedef struct ring_queue {
    queue_header_t *header;
    char *data_zone;
} ring_queue_t;

/* 函数声明 */
extern ring_queue_t *ring_queue_init(int capacity, int blk_size);
extern void ring_queue_put(ring_queue_t *queue, const void *data);
extern void ring_queue_get(ring_queue_t *queue, void *buf);
extern void ring_queue_destroy(ring_queue_t *queue);

#endif

信号量操作封装(sem_util.h / sem_util.c)

c 复制代码
/* sem_util.h */
#ifndef SEM_UTIL_H
#define SEM_UTIL_H

#include <sys/sem.h>
#include <sys/ipc.h>

extern int sem_set_create(int count, unsigned short init_values[]);
extern void sem_wait(int sem_set_id, int index);
extern void sem_post(int sem_set_id, int index);
extern int sem_set_remove(int sem_set_id);

#endif
c 复制代码
/* sem_util.c */
#include "sem_util.h"

/* 创建并初始化信号量集合 */
int sem_set_create(int count, unsigned short init_values[])
{
    key_t ipc_key;
    int sem_set_id;
    int i;

    ipc_key = ftok(".", 202);  /* 注意:使用与共享内存不同的 project ID */
    sem_set_id = semget(ipc_key, count, IPC_CREAT | 0644);
    if (sem_set_id == -1) {
        perror("[ERROR] semget() failed");
        return -1;
    }

    /* 逐个初始化信号量 */
    for (i = 0; i < count; i++) {
        /* Linux 下 semctl 使用 variadic 参数,可直接传 int */
        if (semctl(sem_set_id, i, SETVAL, init_values[i]) == -1) {
            perror("[ERROR] semctl SETVAL failed");
            return -1;
        }
    }
    return sem_set_id;
}

/* P 操作:等待(减一),若当前值为 0 则阻塞 */
void sem_wait(int sem_set_id, int index)
{
    struct sembuf op = {index, -1, 0};
    if (semop(sem_set_id, &op, 1) == -1) {
        perror("[ERROR] semop wait failed");
    }
}

/* V 操作:释放(加一),唤醒等待的进程 */
void sem_post(int sem_set_id, int index)
{
    struct sembuf op = {index, 1, 0};
    if (semop(sem_set_id, &op, 1) == -1) {
        perror("[ERROR] semop post failed");
    }
}

/* 删除信号量集合 */
int sem_set_remove(int sem_set_id)
{
    return semctl(sem_set_id, 0, IPC_RMID, NULL);
}

生产者-消费者测试程序(prod_cons_demo.c)

c 复制代码
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include "ring_queue.h"

/* 测试数据结构:人员信息 */
typedef struct employee {
    int age;
    char name[32];
} employee_t;

int main(void)
{
    int i;
    pid_t child_a, child_b;
    ring_queue_t *queue = ring_queue_init(3, sizeof(employee_t));
    employee_t person;

    printf("=== 环形队列生产者-消费者测试 ===\n");
    printf("队列容量: 3 个 block, 每个 block 大小: %zu 字节\n\n", sizeof(employee_t));

    /* 创建子进程 A ------ 生产者 1(每 1 秒生产一个) */
    child_a = fork();
    if (child_a == 0) {
        for (i = 0; i < 5; i++) {
            strcpy(person.name, "wangwu");
            person.age = 25 + i;
            printf("[生产者A] 写入: name=%s, age=%d\n", person.name, person.age);
            ring_queue_put(queue, &person);
            sleep(1);
        }
        printf("[生产者A] 生产完毕,退出\n");
        return 0;
    }

    /* 创建子进程 B ------ 生产者 2(每 2 秒生产一个) */
    child_b = fork();
    if (child_b == 0) {
        for (i = 0; i < 5; i++) {
            strcpy(person.name, "zhaoliu");
            person.age = 30 + i;
            printf("[生产者B] 写入: name=%s, age=%d\n", person.name, person.age);
            ring_queue_put(queue, &person);
            sleep(2);
        }
        printf("[生产者B] 生产完毕,退出\n");
        return 0;
    }

    /* 父进程:消费者 */
    sleep(1);  /* 等生产者先放一些数据 */

    printf("\n[消费者] 开始读取数据...\n\n");
    for (i = 0; i < 10; i++) {
        ring_queue_get(queue, &person);
        printf("[消费者] 读取到: name=%s, age=%d\n", person.name, person.age);
    }

    /* 等待子进程结束 */
    wait(NULL);
    wait(NULL);

    printf("\n=== 测试结束 ===\n");
    ring_queue_destroy(queue);
    return 0;
}

6.3 执行结果

⚠️ 注意:由于多进程并发执行,输出到终端的顺序由操作系统调度器决定,每次运行结果可能略有不同。以下是在 Linux 系统上的一次典型运行记录:

bash 复制代码
$ gcc -o prod_cons_demo prod_cons_demo.c shm_util.c sem_util.c ring_queue.c
$ ./prod_cons_demo

=== 环形队列生产者-消费者测试 ===
队列容量: 3 个 block, 每个 block 大小: 36 字节

[生产者A] 写入: name=wangwu, age=25
[生产者B] 写入: name=zhaoliu, age=30

[消费者] 开始读取数据...

[消费者] 读取到: name=wangwu, age=25
[生产者A] 写入: name=wangwu, age=26
[消费者] 读取到: name=zhaoliu, age=30
[生产者A] 写入: name=wangwu, age=27
[消费者] 读取到: name=wangwu, age=26
[生产者B] 写入: name=zhaoliu, age=31
[消费者] 读取到: name=wangwu, age=27
[生产者A] 写入: name=wangwu, age=28
[消费者] 读取到: name=zhaoliu, age=31
[生产者A] 写入: name=wangwu, age=29
[消费者] 读取到: name=wangwu, age=28
[生产者B] 写入: name=zhaoliu, age=32
[消费者] 读取到: name=wangwu, age=29
[生产者A] 生产完毕,退出
[消费者] 读取到: name=zhaoliu, age=32
[生产者B] 写入: name=zhaoliu, age=33
[消费者] 读取到: name=zhaoliu, age=33
[生产者B] 写入: name=zhaoliu, age=34
[消费者] 读取到: name=zhaoliu, age=34
[生产者B] 生产完毕,退出

=== 测试结束 ===

要点说明

  • 生产者 A(wangwu)每 1 秒生产一个,共 5 个
  • 生产者 B(zhaoliu)每 2 秒生产一个,共 5 个
  • 消费者异步读取,队列为空时自动阻塞等待
  • 队列容量为 3,队列满时生产者自动阻塞
  • 所有 10 条数据均被正确读取,无数据丢失或重复

6.4 运行过程分析

时间 事件 队列状态 (Block 0/1/2) rd_idx wr_idx
T=0 初始化 空/空/空 0 0
T=1 生产者A写入 wangwu-25 wangwu-25/空/空 0 1
T=2 生产者B写入 zhaoliu-30 wangwu-25/zhaoliu-30/空 0 2
T=2 消费者读取 → wangwu-25 已读/zhaoliu-30/空 1 2
T=3 生产者A写入 wangwu-26 已读/zhaoliu-30/wangwu-26 1 0
T=3 消费者读取 → zhaoliu-30 已读/已读/wangwu-26 2 0
... ... ... ... ...

关键观察:当队列满(3 个 block 都被占用)时,生产者自动阻塞等待;当队列空时,消费者自动阻塞等待。信号量机制确保整个过程不会出现数据覆盖或读取到无效数据的情况。


第七部分:知识对比表

7.1 环形队列 vs 线性队列

对比维度 环形队列 线性队列
数据结构 首尾相连的循环结构 线性的头尾结构
空间复用 ✅ 出队位置自动复用,存储空间循环利用 ❌ 出队后空间闲置,需要搬移数据或"假溢出"
内存效率 高,固定大小无浪费 低,频繁搬移数据或大量空间闲置
操作复杂度 入队/出队均为 O(1) 入队 O(1),出队若需搬移则 O(n)
实现难度 稍复杂,需处理取模回绕 简单直观
适用场景 固定大小的缓冲区、数据流缓存 动态大小的任务队列
典型应用 音频/视频缓冲、网络数据包缓存 普通任务调度、BFS 遍历
类比 回转寿司传送带 超市排队结账

7.2 共享内存 vs 其他 IPC 方式

对比维度 共享内存 管道 (Pipe) 消息队列 (MsgQueue) 套接字 (Socket)
数据传输效率 ⭐⭐⭐⭐⭐ 最高,零拷贝 ⭐⭐⭐ 需内核缓冲 ⭐⭐⭐ 需内核缓冲 ⭐⭐ 需协议栈处理
是否支持多对多 ✅ 是 ❌ 通常一对一 ❌ 通常一对多 ✅ 是
同步机制 ❌ 需额外同步(信号量等) ✅ 自动阻塞 ✅ 自动阻塞 ✅ 自动阻塞
实现复杂度
数据大小限制 受内存限制 受内核缓冲区限制 受消息长度限制 受协议限制
适用场景 大数据量、高性能 IPC 父子进程简单通信 模块间消息传递 跨网络通信
类比 共用白板 传话筒 邮箱 电话

7.3 读操作 vs 写操作

对比维度 写操作 (ring_queue_put) 读操作 (ring_queue_get)
一句话描述 生产者将数据放入队列 消费者从队列取出数据
先检查 队列是否(SEM_FULL) 队列是否(SEM_EMPTY)
阻塞条件 队列满时生产者阻塞 队列空时消费者阻塞
操作指针 更新 wr_idx(写指针) 更新 rd_idx(读指针)
指针回绕 wr_idx = (wr_idx + 1) % capacity rd_idx = (rd_idx + 1) % capacity
释放信号量 V操作 SEM_EMPTY(通知消费者) V操作 SEM_FULL(通知生产者)
类比 厨师往传送带上放寿司 顾客从传送带上取寿司

7.4 三个信号量的职责对比

信号量 初始值 谁 P 操作(减一) 谁 V 操作(加一) 类比
SEM_MUTEX 1 生产者和消费者(取锁) 生产者和消费者(放锁) 洗手间的门锁
SEM_EMPTY 0 消费者(消耗一个数据) 生产者(产出一个数据) 餐盘计数器
SEM_FULL capacity 生产者(占用一个空位) 消费者(腾出一个空位) 空位指示灯

7.5 环形队列使用注意事项

注意事项 说明
队列大小选择 capacity × blk_size 应适当,太小导致频繁阻塞,太大浪费内存
指针回绕 务必使用取模运算 (pos + 1) % capacity,否则指针越界
信号量初始化 只有第一个创建共享内存的进程才初始化信号量,后续进程复用
互斥必要性 共享内存是临界资源,读写操作必须加锁保护
数据块对齐 blk_size 应能容纳实际数据,避免截断
销毁时机 确保所有消费者消费完毕后再销毁,否则数据丢失
进程同步 生产者和消费者必须使用同一组信号量,否则无法同步

附录:项目文件结构

bash 复制代码
项目文件清单:
├── shm_util.h          # 共享内存接口
├── shm_util.c          # 共享内存实现
├── sem_util.h          # 信号量接口
├── sem_util.c          # 信号量实现
├── ring_queue.h        # 环形队列接口
├── ring_queue.c        # 环形队列实现
├── shm_write.c         # 共享内存写端示例
├── shm_read.c          # 共享内存读端示例
├── prod_cons_demo.c    # 生产者-消费者完整测试

最终总结 :环形队列 + 共享内存 + 信号量三者的组合,就像是回转寿司店的传送带(环形队列)+ 公共操作台(共享内存)+ 红绿灯系统(信号量)------传送带循环利用空间,操作台让所有人共享,红绿灯确保秩序井然。这套机制是嵌入式系统和物联网场景下多进程高效通信的经典方案,广泛应用于网络数据包缓存、日志系统、音频视频流处理等场景。

🎯 备注

环形队列(Ring Buffer / Circular Queue)既可以用在线程中,也可以用在进程中。

它本质上只是一种 "数据结构 + 内存布局策略" ,而不是某种专属的通信机制。决定它用在哪里的,不是环形队列本身,而是 "这块内存被谁共享"

💡 一句话总结 环形队列就像一条"循环传送带"。放在同一个车间里,就是线程间 的高速流水线;通过特殊手段架在两个独立厂房之间,就是进程间的零拷贝数据通道。


⚖️ 线程 vs 进程:使用场景与原因对比

虽然都能用,但两者的实现难度、性能和典型场景截然不同:

维度 🧵 用在线程中 🏭 用在进程中
内存基础 天然共享堆/全局内存 必须借助 共享内存 (mmap/shm)
同步开销 极低(Mutex / 原子操作 / 无锁) 较高(需跨进程锁或无锁设计)
实现难度 ⭐⭐ 简单 ⭐⭐⭐⭐ 复杂(需处理地址映射、对齐)
核心优势 避免频繁 malloc/free,减少锁竞争 真正的零拷贝,绕过内核态数据搬运
典型场景 生产者-消费者模型、日志缓冲、任务池 音视频采集/播放、高频交易、驱动与用户态通信
著名案例 Java Disruptor、C++ boost::lockfree FFmpeg、DPDK、ALSA 音频子系统、ROS2
相关推荐
pt10431 小时前
思科网络自动化-API常用数据编码格式(JSON/XML/YAML)课程与实践
linux·服务器·网络
kdxiaojie1 小时前
Linux 驱动研究 —— I2C (5)
linux·运维·笔记·学习
会周易的程序员2 小时前
从零构建多核CPU负载自适应控制系统
linux·c++·笔记·物联网·测试工具
ZXXstarstar2 小时前
2026年电池安全使用年限的量化决策:什么时间到期最需要替换?
linux·运维·服务器·电池
网络小白不怕黑2 小时前
httpd监控
linux·运维·服务器
weixin_307779134 小时前
Linux下Docker Compose里运行PostgreSQL数据库故障诊断Shell脚本
linux·运维·数据库·docker·postgresql
AI创界者4 小时前
安全测试环境搭建:在 Kali Linux 中部署与配置开源 WebShell 管理工具 AntSword(蚁剑)
linux·运维·安全
猪头少年不会遇见算法学姐4 小时前
Makefile快速上手笔记
linux·学习
qiu_lovejun9984 小时前
linux安装docker和redis和rabbitmq和nginx和rocketmq和kafka
linux·redis·docker·kafka·rabbitmq·rocketmq