Linux进程间通信-共享内存

systemV共享内存

一、什么是共享内存?

共享内存是 Linux 里效率最高的进程间通信(IPC)方式,你可以把它理解成:

多个进程之间的「公共黑板」,大家都能直接在这块黑板上读写数据,不用像管道那样绕内核的弯儿,所以速度是所有 IPC 里最快的。

它的核心逻辑,就是让多个进程,把同一块物理内存区域 ,映射到各自的虚拟地址空间里。进程读写共享内存,就和读写自己的普通变量一样,数据不需要经过内核中转

二、它的工作原理

1. 先在内核里划分一块物理内存

内核先在物理内存中,分配一块连续 / 不连续的区域 ,作为「公共黑板」,这一步由系统调用 **shmget()**完成。

2. 把物理内存映射到进程的虚拟地址空间

上图进程 A 和进程 B,都通过 shmat() 系统调用,把这块物理内存,映射到自己的虚拟地址空间里。

  • 对进程 A 来说,它看到的是自己地址空间里的一块普通内存
  • 对进程 B 来说,它看到的也是自己地址空间里的一块普通内存
  • 但这两块虚拟内存,最终指向的是同一块物理内存

3. 数据「零拷贝」,直接读写

进程 A 写数据时,直接往共享内存里写;进程 B 读数据时,直接从共享内存里读。

  • 对比你之前用的命名管道:数据要走两次拷贝(用户空间→内核缓冲区→另一个进程的用户空间),相当于「写信→交给邮局→邮局转交给对方」
  • 共享内存:数据不用经过内核中转,直接在物理内存里读写,相当于「大家共用一块黑板,写了别人直接就能看到」,所以效率是最高的。

4. 生命周期跟着内核走

共享内存的生命周期和内核绑定,就算进程 A 和 B 都退出了,这块共享内存还会一直存在,直到你手动用 shmctl() 删除它,或者系统重启。

三、它的缺点 & 注意事项

共享内存虽然快,但有个致命问题:多个进程可以同时读写这块内存,会出现数据竞争。就像两个人同时在黑板上写字,内容会互相覆盖,数据直接乱掉。

所以,共享内存必须配合同步机制使用,比如:

  • 信号量(Semaphore)
  • 互斥锁(Mutex)来保证同一时间,只有一个进程在读写共享内存,避免数据混乱。

四、和「命名管道」对比

特性 命名管道(FIFO) 共享内存
速度 慢(两次数据拷贝,需要内核中转) 最快(零拷贝,直接读写)
通信方向 半双工,单向传递 双向,直接读写
同步机制 自带阻塞 / 非阻塞机制,不用手动处理 必须手动加同步(信号量 / 互斥锁)
生命周期 进程退出后自动消失 内核管理,需手动删除

五、核心接口介绍

Linux 提供两套共享内存 API:

  • System V 共享内存:传统、经典,兼容性好
  • POSIX 共享内存 :较新,更简洁,常与 mmap 配合使用

下面详细介绍 System V 版的核心接口:

1. 创建 / 获取共享内存:shmget()

头文件#include <sys/shm.h>

cpp 复制代码
int shmget(key_t key, size_t size, int shmflg);
参数 说明
key IPC 键值,用来唯一标识共享内存。可以用 IPC_PRIVATE(私有,父子进程用),或 ftok() 生成公共键值
size 共享内存大小(字节),内核会自动向上对齐到页大小(通常 4KB)
shmflg 权限标志,和文件权限类似,常用 IPC_CREAT(不存在则创建) + 0666(读写权限)

返回值 :成功返回共享内存标识符 shmid,失败返回 -1

2. 生成唯一键值:ftok()

头文件#include <sys/ipc.h>

cpp 复制代码
key_t ftok(const char *pathname, int proj_id);
  • 作用:把一个文件路径和一个项目 ID,转换成唯一的 key_t 值,让不同进程获取同一个共享内存
  • 注意:pathname 必须是一个存在的文件路径,proj_id 取 1-255 之间的整数即可

3. 映射共享内存到进程地址空间:shmat()

头文件#include <sys/shm.h>

cpp 复制代码
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数 说明
shmid shmget() 返回的共享内存标识符
shmaddr 指定映射的虚拟地址,填 NULL 让内核自动分配(推荐)
shmflg 映射标志,0 表示读写,SHM_RDONLY 表示只读

返回值 :成功返回映射后的虚拟地址指针,失败返回 (void*)-1

4. 解除进程与共享内存的映射:shmdt()

头文件:#include <sys/shm.h>

cpp 复制代码
int shmdt(const void *shmaddr);
  • 作用:解除当前进程和共享内存的映射,进程退出时也会自动解除
  • 参数:shmat() 返回的虚拟地址指针
  • 返回值:成功返回 0,失败返回 -1

5. 控制 / 删除共享内存:shmctl()

头文件#include <sys/shm.h>

cpp 复制代码
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数 说明
shmid 共享内存标识符
cmd 控制命令,常用:IPC_RMID(删除共享内存)、IPC_STAT(获取状态)
buf 状态结构体指针,填 NULL 即可

返回值 :成功返回 0,失败返回 -1

命令行操作:

ipcs -m:查看共享内存

ipcrm -m ID:删除共享内存

指令本质是运行在用户空间的。

删除,控制共享内存在用户层,我们不能使用key!key未来只给内核进行区分唯一性!要用shmid进行管理内存。

补充:必须配合的同步机制(信号量)

共享内存没有自带同步,多个进程同时读写会出现数据竞争,所以必须搭配 System V 信号量使用,核心接口:

  • semget():创建 / 获取信号量
  • semop():P/V 操作(加锁 / 解锁)
  • semctl():删除信号量

C++封装共享内存接口

1. 公共头文件 comm.hpp

cpp 复制代码
#ifndef __COMM_HPP__
#define __COMM_HPP__

#include <iostream>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
#include <cstring>

using namespace std;

// 生成key用的路径和项目ID
#define PATHNAME "./comm.hpp"
#define PROJ_ID 0x6666

// 共享内存大小
#define SHM_SIZE 4096

// 信号量操作函数
union semun {
    int val;
};

// P操作(加锁)
void semP(int semid)
{
    struct sembuf buf;
    buf.sem_num = 0;
    buf.sem_op = -1;
    buf.sem_flg = 0;
    semop(semid, &buf, 1);
}

// V操作(解锁)
void semV(int semid)
{
    struct sembuf buf;
    buf.sem_num = 0;
    buf.sem_op = 1;
    buf.sem_flg = 0;
    semop(semid, &buf, 1);
}

// 创建/获取信号量
int createSem()
{
    key_t key = ftok(PATHNAME, PROJ_ID);
    int semid = semget(key, 1, IPC_CREAT | 0666);
    if (semid == -1)
    {
        cerr << "semget error" << endl;
        exit(1);
    }
    // 初始化信号量为1
    union semun su;
    su.val = 1;
    semctl(semid, 0, SETVAL, su);
    return semid;
}

// 创建/获取共享内存
int createShm()
{
    key_t key = ftok(PATHNAME, PROJ_ID);
    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
    if (shmid == -1)
    {
        cerr << "shmget error" << endl;
        exit(1);
    }
    return shmid;
}

#endif

2. 服务端 server.cc(读取共享内存)

cpp 复制代码
#include "comm.hpp"

int main()
{
    // 1. 创建共享内存和信号量
    int shmid = createShm();
    int semid = createSem();

    // 2. 映射共享内存到当前进程
    char* shmaddr = (char*)shmat(shmid, NULL, 0);
    if (shmaddr == (char*)-1)
    {
        cerr << "shmat error" << endl;
        exit(1);
    }

    // 3. 循环读取共享内存
    while (true)
    {
        semP(semid); // 加锁
        if (strlen(shmaddr) > 0)
        {
            cout << "Server recv: " << shmaddr << endl;
            memset(shmaddr, 0, SHM_SIZE); // 读完清空
        }
        semV(semid); // 解锁
        sleep(1);
    }

    // 4. 解除映射(实际不会走到这里)
    shmdt(shmaddr);
    shmctl(shmid, IPC_RMID, NULL);
    semctl(semid, 0, IPC_RMID);
    return 0;
}

3. 客户端 client.cc(写入共享内存)

cpp 复制代码
#include "comm.hpp"

int main()
{
    // 1. 获取共享内存和信号量
    int shmid = createShm();
    int semid = createSem();

    // 2. 映射共享内存到当前进程
    char* shmaddr = (char*)shmat(shmid, NULL, 0);
    if (shmaddr == (char*)-1)
    {
        cerr << "shmat error" << endl;
        exit(1);
    }

    // 3. 写入数据到共享内存
    int cnt = 0;
    while (true)
    {
        semP(semid); // 加锁
        snprintf(shmaddr, SHM_SIZE, "Hello Shm! cnt: %d", cnt++);
        cout << "Client send: " << shmaddr << endl;
        semV(semid); // 解锁
        sleep(1);
    }

    // 4. 解除映射(实际不会走到这里)
    shmdt(shmaddr);
    return 0;
}

结果:

总结

读写共享内存没有调用系统调用!共享内存存在于堆栈之间,属于用户空间,用户可直接使用。

共享内存是进程间通信中,速度最快的!

1.映射之后,读写直接被对方看到!

2.不需要进行系统调用获取或者写入内容,直接以指针,地址的方法进行访问。通信双方没有所谓的"同步进制"(数据不一致)。共享内存没有保护进制。

共享内存创建的时候,它的大小必须是4KB的整数倍。

相关推荐
AbandonForce1 小时前
Linux权限深入解读
linux·运维·服务器
哎呦,帅小伙哦1 小时前
Nanomsg usock 模块:Socket 选项与错误码介绍
linux·中间件·nanomsg
lbb 小魔仙1 小时前
Docker一键部署 EasyNode 面板,随时随地可视化管理服务器
服务器·docker·容器
Elastic 中国社区官方博客1 小时前
Hacknight Beijing:基于阿里云与 Elastic 构建 AI Agents
大数据·运维·人工智能·elasticsearch·搜索引擎·阿里云·云计算
草莓熊Lotso1 小时前
【Linux网络】深入理解 HTTP 协议(一):从基础概念到 URL 编码解码
linux·网络·c++·网络协议·http·软件工程
一号弯1 小时前
用NAVICAT访问非本地服务器的报错问题
运维·服务器
能摆一天是一天2 小时前
windows docker 部署openfire
运维·docker·容器
jingling5552 小时前
Flutter | 从基本跳转到路由守卫
服务器·前端·网络·flutter·前端框架