Linux 进程间通信:共享内存与消息队列完全指南

引言

在Linux系统编程中,进程间通信(IPC)是多进程协作的核心技术。前面我们学习了管道,今天我们将深入讲解另外三种重要的IPC机制:共享内存信号量消息队列

这三种机制各有特点:

  • 共享内存:最高效的数据交换方式(无需数据拷贝)

  • 信号量:进程同步机制(协调资源访问顺序)

  • 消息队列:结构化数据传递(按类型接收消息)

它们与管道一起,构成了Linux IPC的核

第一部分:共享内存(Shared Memory)

一、共享内存的基本概念

共享内存是最快的IPC机制。它的原理是:将同一块物理内存映射到多个进程的逻辑地址空间中,进程可以直接读写这块内存,无需数据拷贝。

心体系。通过ipcs命令可以查看系统中的所有IPC资源。

二、共享内存的操作流程

步骤 操作 函数 说明
1 创建/获取 shmget() 创建共享内存或获取已存在的
2 映射 shmat() 将共享内存映射到进程地址空间
3 使用 指针读写 直接通过指针访问共享内存
4 断开映射 shmdt() 解除当前进程的映射(非删除)
5 删除 shmctl() 彻底删除共享内存

三、共享内存接口详解

1. shmget------创建/获取共享内存
cpp 复制代码
#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);
// 返回值:成功返回共享内存ID,失败返回-1
参数 说明
key 唯一标识符,多个进程通过相同的key获取同一块共享内存
size 共享内存大小(字节)
shmflg 标志位:IPC_CREAT(创建)、0600(权限)
2. shmat------映射共享内存
cpp 复制代码
void *shmat(int shmid, const void *shmaddr, int shmflg);
// 返回值:成功返回映射地址,失败返回(void*)-1
参数 说明
shmid shmget返回的共享内存ID
shmaddr 指定映射地址,通常设为NULL(让系统自动选择)
shmflg 标志位,通常设为0
3. shmdt------断开映射
cpp 复制代码
int shmdt(const void *shmaddr);
// 返回值:成功返回0,失败返回-1
4. shmctl------控制共享内存
cpp 复制代码
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
// 返回值:成功返回0,失败返回-1
参数 说明
cmd IPC_RMID(删除)、IPC_STAT(获取状态)
buf 状态结构体,删除时可设为NULL

四、共享内存代码示例

写入端程序(write.c)
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define SHM_SIZE 128
#define KEY 1234

int main() {
    // 1. 创建共享内存
    int shmid = shmget(KEY, SHM_SIZE, IPC_CREAT | 0600);
    if (shmid == -1) {
        perror("shmget error");
        exit(1);
    }
    
    // 2. 映射共享内存
    char *shmaddr = (char *)shmat(shmid, NULL, 0);
    if (shmaddr == (void *)-1) {
        perror("shmat error");
        exit(1);
    }
    
    // 3. 写入数据
    printf("请输入消息: ");
    fgets(shmaddr, SHM_SIZE, stdin);
    shmaddr[strlen(shmaddr) - 1] = '\0';  // 去除换行符
    
    printf("写入完成: %s\n", shmaddr);
    
    // 4. 断开映射
    shmdt(shmaddr);
    
    return 0;
}
读取端程序(read.c)
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define SHM_SIZE 128
#define KEY 1234

int main() {
    // 1. 获取共享内存(已存在)
    int shmid = shmget(KEY, SHM_SIZE, 0600);
    if (shmid == -1) {
        perror("shmget error");
        exit(1);
    }
    
    // 2. 映射共享内存
    char *shmaddr = (char *)shmat(shmid, NULL, 0);
    if (shmaddr == (void *)-1) {
        perror("shmat error");
        exit(1);
    }
    
    // 3. 读取数据
    printf("读取到: %s\n", shmaddr);
    
    // 4. 断开映射
    shmdt(shmaddr);
    
    return 0;
}

运行结果:

# 先运行写入端
./write
请输入消息: hello world
写入完成: hello world

# 再运行读取端
./read
读取到: hello world

五、查看与管理共享内存

查看所有IPC资源

ipcs

查看共享内存

ipcs -m

输出示例:

------ Shared Memory Segments --------

key shmid owner perms bytes nattch status

0x000004d2 0 user 600 128 0

删除共享内存

ipcrm -m [shmid]

第二部分:共享内存与信号量的综合应用

一、为什么需要信号量?

共享内存本身不提供同步机制。当多个进程同时读写共享内存时,可能会出现竞态条件。例如:读取端在写入端还未写入数据时就读取,会读到无效数据。

解决方案: 使用信号量实现进程同步,控制读写顺序。

二、同步逻辑设计

信号量 初始值 控制目标 操作逻辑
SEM_WRITE 1 写端写入权限 写入前P,写入后V
SEM_READ 0 读端读取权限 读取前P,读取后V

三、完整代码实现

sem.h(头文件):

cpp 复制代码
#ifndef SEM_H
#define SEM_H

int sem_init(int key, int nsems, int *init_vals);
void sem_p(int semid, int sem_num);
void sem_v(int semid, int sem_num);
void sem_destroy(int semid);

#endif

sem.c(实现文件):

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/sem.h>

union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};

int sem_init(int key, int nsems, int *init_vals) {
    int semid;
    
    // 尝试创建信号量集
    semid = semget(key, nsems, IPC_CREAT | IPC_EXCL | 0600);
    
    if (semid == -1) {
        // 已存在,直接获取
        semid = semget(key, nsems, 0600);
    } else {
        // 创建成功,初始化信号量值
        union semun a;
        for (int i = 0; i < nsems; i++) {
            a.val = init_vals[i];
            semctl(semid, i, SETVAL, a);
        }
    }
    
    return semid;
}

void sem_p(int semid, int sem_num) {
    struct sembuf buf = {sem_num, -1, SEM_UNDO};
    if (semop(semid, &buf, 1) == -1) {
        perror("P操作失败");
    }
}

void sem_v(int semid, int sem_num) {
    struct sembuf buf = {sem_num, 1, SEM_UNDO};
    if (semop(semid, &buf, 1) == -1) {
        perror("V操作失败");
    }
}

void sem_destroy(int semid) {
    semctl(semid, 0, IPC_RMID);
}

写入端(write_sync.c):

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "sem.h"

#define SHM_SIZE 128
#define SHM_KEY 1234
#define SEM_KEY 5678
#define SEM_NUMS 2

int main() {
    // 初始化信号量:SEM_WRITE=1(写权限),SEM_READ=0(读权限)
    int init_vals[2] = {1, 0};
    int semid = sem_init(SEM_KEY, SEM_NUMS, init_vals);
    
    // 创建共享内存
    int shmid = shmget(SHM_KEY, SHM_SIZE, IPC_CREAT | 0600);
    char *shmaddr = (char *)shmat(shmid, NULL, 0);
    
    while (1) {
        sem_p(semid, 0);  // P(写权限) - 申请写入
        
        printf("请输入消息: ");
        fgets(shmaddr, SHM_SIZE, stdin);
        shmaddr[strlen(shmaddr) - 1] = '\0';
        
        if (strcmp(shmaddr, "end") == 0) {
            sem_v(semid, 1);  // 通知读端结束
            break;
        }
        
        printf("写入: %s\n", shmaddr);
        sem_v(semid, 1);  // V(读权限) - 通知读取端
    }
    
    shmdt(shmaddr);
    return 0;
}

读取端(read_sync.c):

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "sem.h"

#define SHM_SIZE 128
#define SHM_KEY 1234
#define SEM_KEY 5678
#define SEM_NUMS 2

int main() {
    // 获取信号量
    int init_vals[2] = {0, 0};
    int semid = sem_init(SEM_KEY, SEM_NUMS, init_vals);
    
    // 获取共享内存
    int shmid = shmget(SHM_KEY, SHM_SIZE, 0600);
    char *shmaddr = (char *)shmat(shmid, NULL, 0);
    
    while (1) {
        sem_p(semid, 1);  // P(读权限) - 等待写入
        
        if (strcmp(shmaddr, "end") == 0) break;
        
        printf("读取到: %s\n", shmaddr);
        
        sem_v(semid, 0);  // V(写权限) - 释放写入权限
    }
    
    // 断开映射并删除信号量
    shmdt(shmaddr);
    sem_destroy(semid);
    
    return 0;
}

编译与运行:

# 编译
gcc sem.c write_sync.c -o write
gcc sem.c read_sync.c -o read

# 先启动读取端(会阻塞等待数据)
./read &

# 再启动写入端
./write

第三部分:消息队列(Message Queue)

一、消息队列的基本概念

消息队列是内核维护的消息链表 ,每个消息有独立的类型标识符,接收方可以按类型选择性读取。数据会一直驻留在队列中,直到被读取或系统重启。

二、消息队列接口

操作 函数 说明
创建/获取 msgget() 创建或获取消息队列
发送消息 msgsnd() 向队列发送消息
接收消息 msgrcv() 从队列接收消息(可按类型过滤)
控制 msgctl() 删除消息队列

三、消息队列代码示例

消息结构体定义
cpp 复制代码
// 消息结构体(首成员必须是long类型)
struct msgbuf {
    long mtype;           // 消息类型(必须 ≥ 1)
    char mtext[128];      // 消息内容
};
发送端程序(send.c)
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#define KEY 1234

struct msgbuf {
    long mtype;
    char mtext[128];
};

int main() {
    // 1. 创建消息队列
    int msgid = msgget(KEY, IPC_CREAT | 0600);
    if (msgid == -1) {
        perror("msgget error");
        exit(1);
    }
    
    struct msgbuf msg;
    
    // 2. 发送多条不同类型的消息
    msg.mtype = 1;
    strcpy(msg.mtext, "Hello, type 1");
    msgsnd(msgid, &msg, sizeof(msg.mtext), 0);
    
    msg.mtype = 2;
    strcpy(msg.mtext, "Hello, type 2");
    msgsnd(msgid, &msg, sizeof(msg.mtext), 0);
    
    msg.mtype = 1;
    strcpy(msg.mtext, "Another type 1");
    msgsnd(msgid, &msg, sizeof(msg.mtext), 0);
    
    printf("消息发送完成\n");
    
    return 0;
}
接收端程序(recv.c)
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#define KEY 1234

struct msgbuf {
    long mtype;
    char mtext[128];
};

int main() {
    // 1. 获取消息队列
    int msgid = msgget(KEY, 0600);
    if (msgid == -1) {
        perror("msgget error");
        exit(1);
    }
    
    struct msgbuf msg;
    
    // 2. 只接收类型为1的消息(类型2的消息会保留在队列中)
    printf("=== 接收类型1的消息 ===\n");
    for (int i = 0; i < 2; i++) {
        msgrcv(msgid, &msg, sizeof(msg.mtext), 1, 0);
        printf("收到: %s\n", msg.mtext);
    }
    
    // 3. 再接收类型2的消息
    printf("=== 接收类型2的消息 ===\n");
    msgrcv(msgid, &msg, sizeof(msg.mtext), 2, 0);
    printf("收到: %s\n", msg.mtext);
    
    // 4. 删除消息队列
    msgctl(msgid, IPC_RMID, NULL);
    
    return 0;
}

运行结果:

# 运行发送端
./send
消息发送完成

# 运行接收端
=== 接收类型1的消息 ===
收到: Hello, type 1
收到: Another type 1
=== 接收类型2的消息 ===
收到: Hello, type 2

四、消息类型匹配规则

msgtyp 行为
> 0 只接收该类型消息(如msgtyp=1只接收类型1)
= 0 接收队列中第一个消息(FIFO,不区分类型)
< 0 接收小于等于|msgtyp|的最小类型消息

五、查看与管理消息队列

查看消息队列

ipcs -q

输出示例:

------ Message Queues --------

key msqid owner perms used-bytes messages

0x000004d2 0 user 600 256 2

删除消息队列

ipcrm -q [msqid]

总结

一、共享内存核心要点

操作 函数 说明
创建 shmget() 指定key和大小
映射 shmat() 返回内存指针
断开 shmdt() 解除映射
删除 shmctl(IPC_RMID) 彻底删除

二、消息队列核心要点

操作 函数 说明
创建 msgget() 指定key
发送 msgsnd() 消息必须有类型(≥1)
接收 msgrcv() 可按类型过滤
删除 msgctl(IPC_RMID) 删除队列

三、共享内存 vs 消息队列

特性 共享内存 消息队列
数据存储 物理内存 内核链表
通信模式 双向 双向
同步机制 需要信号量配合 自带类型过滤
数据持久性 直到删除 直到删除
效率 最高(无数据拷贝) 中等
适用场景 大数据量快速交换 结构化消息传递

四、完整的IPC体系

IPC机制 主要功能 参考文章
管道/有名管道 简单数据流传输 IPC专题(一)
信号量 进程同步 IPC专题(二)
共享内存 高效数据交换 本文
消息队列 结构化消息传递 本文

共享内存和消息队列是Linux IPC的重要组成部分。共享内存解决了数据交换效率问题,消息队列解决了结构化消息传递问题。理解它们的工作原理和使用方法,是掌握多进程编程的关键。

学习建议:

  1. 理解共享内存的映射原理(物理内存映射到逻辑地址)

  2. 注意共享内存需要配合信号量实现同步

  3. 掌握消息队列的类型匹配规则

  4. 学会使用ipcs查看和管理IPC资源

相关推荐
计算机安禾1 小时前
【Linux从入门到精通】第27篇:文本处理三剑客(上)——grep 正则表达式实战
linux·运维·正则表达式
码到成功>_<1 小时前
Linux中grep命令使用说明
linux
minji...1 小时前
Linux 网络套接字编程(六)TCP的通信是全双工的,自定义协议的定制,序列化和反序列化
linux·运维·服务器·网络·c++
小王C语言1 小时前
【linux进程信号】————产生信号:signal自定义信号处理动作(自定义捕捉)、前后台进程、产生信号的方式(函数、软条件、硬件异常)....等等
运维·服务器·前端
Gauss松鼠会2 小时前
效率起飞!GaussDB 管理平台(TPOPS)升级指南
服务器·数据库·性能优化·gaussdb·经验总结
晚风予卿云月2 小时前
【linux】僵尸进程与孤儿进程
linux·运维·服务器
hhb_6182 小时前
Tcl脚本自动化运维实操落地案例详解
运维·网络·自动化
故事还在继续吗2 小时前
Linux cgroup 使用指南:从原理到实践
linux·运维·服务器
csdn2015_2 小时前
lambdaQuery 加 or
java·linux·服务器