Linux

关注公众号 jb51net

关闭
首页 > 网站技巧 > 服务器 > Linux > Linux进程间通信之System V IPC共享内存

Linux进程间通信之System V IPC——共享内存原理

作者:Ronin305

还在为进程间通信效率低而烦恼吗?本文深入剖析共享内存机制,从核心概念到shmget、shmat等系统调用,带你掌握零拷贝通信的精髓,通过代码示例和与管道对比,让你彻底理解共享内存为何是最高效的IPC方式,并学会如何规避同步与安全陷阱

前文中我们介绍了管道——匿名管道和命名管道来实现进程间通信,在介绍怎么进行通信时,我们有提到过不止管道的方式进行通信,还有System V IPC,今天这篇文章我们就来学习一下System V IPC中的共享内存

1. 为何引入共享内存?——管道通信的局限性

管道(匿名/命名管道)作为传统IPC机制存在显著缺陷:

  1. 数据拷贝开销大:管道需通过内核缓冲区中转,数据需从用户空间→内核→用户空间两次拷贝
  2. 单向通信限制:匿名管道仅支持单向数据流,双向通信需建立两个管道
  3. 效率瓶颈:频繁读写时内核缓冲区切换成为性能瓶颈
  4. 适用场景有限:命名管道虽突破亲缘关系限制,但仍依赖文件系统路径,且同步机制弱

共享内存的破局:

通过多进程直接访问同一物理内存区域,消除数据拷贝,实现零复制(Zero-Copy)通信,速度提升10-100倍

2.共享内存核心概念:打破进程隔离的革命性设计

共享内存是 System V IPC(Inter-Process Communication)机制的一种,它允许多个不相关的进程(父子进程或完全独立的进程)访问同一块物理内存区域。这是最快的进程间通信(IPC)形式,因为它完全避免了内核空间和用户空间之间数据的复制。

本质定义

物理内存共享:多个进程通过页表映射,直接访问同一块物理内存区域,实现零拷贝数据交换。

逻辑视图:在进程虚拟地址空间中表现为普通内存段(如malloc分配),实则由操作系统管理共享物理页。

int *shared_counter = (int*)shmat(shm_id, NULL, 0);
*shared_counter += 1; // 修改对其他进程立即可见
 

核心特性

特性技术内涵
高效性消除内核中转与数据拷贝,吞吐量达管道通信的 5-20倍(GB/s级)
双向性支持多进程并发读写(需同步机制保障)
非亲缘性任意进程(无关父子关系)可通过唯一标识符(Key)访问
持久性生命周期独立于进程,需显式销毁(否则残留内核直至重启)
无内置同步需开发者结合信号量/互斥锁解决竞态条件(如写覆盖、脏读)

与进程地址空间的融合

// 进程视角:共享内存如同本地变量
char *shm_ptr = shmat(shm_id, NULL, 0);  // 映射共享内存到虚拟地址空间
strcpy(shm_ptr, "Hello from Process A");  // 直接写入

关键理解:

3. 共享内存工作原理

那操作系统是怎么管理共享内存的呢?先描述再组织

通过一个内核结构体来描述共享内存,再由操作系统统一管理这些内核结构体

共享内存数据结构:

struct shmid_ds {
    struct ipc_perm shm_perm; /* operation perms */
    int shm_segsz; /* size of segment (bytes) */
    __kernel_time_t shm_atime; /* last attach time */
    __kernel_time_t shm_dtime; /* last detach time */
    __kernel_time_t shm_ctime; /* last change time */
    __kernel_ipc_pid_t shm_cpid; /* pid of creator */
    __kernel_ipc_pid_t shm_lpid; /* pid of last operator */
    unsigned short shm_nattch; /* no. of current attaches */
    unsigned short shm_unused; /* compatibility */
    void shm_unused2; /* ditto - used by DIPC */
    void shm_unused3; /* unused */
};

一、管理机制:描述与组织的双重架构

1. 描述层:内核数据结构定义

每个共享内存段由两个关键结构体描述:

(用户可见元信息)

用户提供的结构体包含基础属性,但内核实际使用扩展结构体:

struct shmid_ds {
    struct ipc_perm shm_perm;   // 权限控制(UID/GID/模式)
    size_t shm_segsz;           // 段大小(字节)
    time_t shm_atime;           // 最后一次映射时间
    time_t shm_dtime;           // 最后一次解除映射时间
    time_t shm_ctime;           // 最后一次修改时间
    pid_t shm_cpid;             // 创建者PID
    pid_t shm_lpid;             // 最后一次操作者PID
    unsigned short shm_nattch;  // 当前映射进程数
    // ... 兼容性保留字段
};

struct shmid_kernel(内核私有管理结构)

struct shmid_kernel {
    struct kern_ipc_perm shm_perm;  // IPC权限控制块
    struct file *shm_file;          // 关联的shm文件对象
    unsigned long shm_nattch;       // 映射计数
    size_t shm_segsz;               // 段大小
    struct pid *shm_cprid;          // 创建者PID(内核态)
    struct pid *shm_lprid;          // 最后操作者PID
    // ... 其他内核级字段
};

关键扩展:

2. 组织层:全局管理架构

内核通过三级结构统一管理所有共享内存段:

层级数据结构功能
全局入口struct ipc_ids shm_ids维护系统内所有共享内存的ID空间
ID索引层struct kern_ipc_perm*[]指针数组,每个元素指向一个shmid_kernel
共享内存实例struct shmid_kernel描述单个共享内存段的完整状态

动态管理演进:

二、内核操作流程剖析

1. 创建共享内存(shmget)

int shmget(key_t key, size_t size, int shmflg) {
    // 1. 根据key查找或新建shmid_kernel
    // 2. 在shm文件系统中创建匿名文件
    struct file *file = shmem_file_setup("SYSV<key>", size, flags);
    // 3. 初始化shmid_kernel:绑定file,设置size/权限等
    // 4. 将shmid_kernel插入全局红黑树
}

关键动作:

2. 映射共享内存(shmat)

void *shmat(int shmid, void *addr, int flag) {
    // 1. 根据shmid找到shmid_kernel
    struct shmid_kernel *shp = find_shm(shmid);
    // 2. 在进程地址空间创建VMA区域
    vma = vm_area_alloc(current->mm);
    vma->vm_file = shp->shm_file;  // 关联shm文件
    vma->vm_ops = &shmem_vm_ops;   // 设置内存操作函数
    // 3. 更新shm_nattch引用计数
    shp->shm_nattch++;
}

虚拟内存映射:

3. 生命周期管理

操作内核行为
删除(shmctl(IPC_RMID))标记为SHM_DEST,当shm_nattch=0时触发物理内存回收
进程退出自动调用shmdt解除映射,递减shm_nattch
系统重启所有共享内存被销毁(因物理内存重置)

三、物理内存与虚拟地址的协同管理

1. 物理内存分配

2. 多进程共享的一致性

机制原理
写时复制(COW)若进程尝试写入只读映射的共享内存,触发COW生成私有副本
内存屏障使用mb()/rmb()指令保证多核CPU缓存一致性
原子操作引用计数(如shm_nattch)通过原子指令增减

四、与传统文件映射的差异

特性共享内存文件映射(mmap)
数据持久性进程退出后数据消失文件内容持久化到磁盘
同步机制需手动同步(如msync)内核自动回写脏页
初始化成本无磁盘I/O需加载文件数据到内存
适用场景高频临时数据交换持久化数据共享

五、设计哲学总结

抽象与隔离:

零拷贝思想:

动态扩展性:

资源自治:

共享内存的工作原理 (关键步骤)

创建或获取共享内存段 (shmget):

将共享内存段附加到进程地址空间 (shmat):

使用共享内存:

关键点:共享内存本身不提供任何同步机制! 多个进程同时读写同一区域会导致数据竞争(Race Condition) 和数据不一致。必须结合其他同步机制使用:

分离共享内存段 (shmdt):

控制/销毁共享内存段 (shmctl):

其他 cmd 包括获取/设置段信息 (IPC_STAT, IPC_SET)。

4. 共享内存函数

4.1shmget函数核心解析(系统级共享内存管理)

shmget是System V IPC中创建或获取共享内存段的核心函数,其本质是向内核申请一块多进程可共同访问的物理内存区域。

1. 函数原型与基础机制

#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

内核行为:

2. 参数解析

参数技术内涵内核行为
key唯一标识符:• IPC_PRIVATE:强制创建新段• ftok():基于文件路径+项目ID生成红黑树检索 key,存在则返回 shmid;不存在且 IPC_CREAT 置位则创建新段
size内存段大小(字节):• 新创建时需 >0• 自动对齐页大小(4KB)调用 shmem_file_setup() 在 tmpfs 创建匿名文件,映射物理页
shmflg位掩码标志:• 权限位:低9位(如 0666)• IPC_CREAT:不存在则创建• IPC_EXCL:存在则报错初始化 shmid_ds.shm_perm 结构,设置 UID/GID 和权限

高级标志:

3. 内核数据结构初始化

创建新段时,内核初始化struct shmid_ds元数据结构:

struct shmid_ds {
    struct ipc_perm shm_perm;   // 权限控制块
    size_t shm_segsz;           // 段大小(=size参数)
    time_t shm_atime;           // 最后一次attach时间
    time_t shm_dtime;           // 最后一次detach时间
    time_t shm_ctime;           // 最后一次修改时间
    pid_t shm_cpid;             // 创建者PID
    pid_t shm_lpid;             // 最后操作者PID
    unsigned short shm_nattch;  // 当前附加进程数
};

初始化规则:

  1. shm_perm.cuid/uid = 调用进程有效UID
  2. shm_perm.cgid/gid = 调用进程有效GID
  3. shm_perm.mode = shmflg的低9位权限
  4. shm_atime/shm_dtime = 0(未映射)
  5. shm_ctime = 当前系统时间

物理内存分配:内核调用alloc_pages()分配连续物理页,内容初始化为0

4. 错误处理

错误码触发条件解决方案
EACCES权限不足检查 shmflg 权限位
EEXISTIPC_CREAT+IPC_EXCL 且段已存在移除 IPC_EXCL 或更换 key
EINVALsize 无效(> SHMMAX 或 < 页大小)调整 size 为页大小整数倍
ENOENTkey 不存在且未设 IPC_CREAT增加 IPC_CREAT 标志

系统限制:

深入解析shmget的key参数:跨进程共享内存的标识核心

一、key 参数的核心作用与设计哲学

key 是 shmget 函数中唯一标识共享内存段的整数标识符,其本质是操作系统用于区分不同共享内存段的全局键值。它的作用类似于文件系统中的路径名,但以整数形式存在,核心价值在于:

  1. 跨进程标识:不同进程通过相同 key 访问同一物理内存区域。
  2. 资源复用:避免重复创建相同内存段,减少资源浪费。
  3. 权限控制:与 shmflg 权限位协同管理进程访问权限。

设计哲学:key 体现了操作系统对共享资源的 “命名空间抽象” —— 用轻量级整数替代复杂路径,实现高效资源定位。

二、key 的生成方式与典型场景(附代码示例)

1. ftok() 动态生成(推荐方案)

#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);

机制:

基于文件路径(pathname)和项目ID(proj_id)生成唯一 key。

原理:

取文件索引节点号(st_ino)的低8位 + 设备号(st_dev)的低8位 + proj_id 的低8位,组合成32位整数。

示例:

// 服务端创建共享内存
key_t config_key = ftok("/etc/app_config", 123);  // 基于配置文件生成key
int shmid = shmget(config_key, 4096, IPC_CREAT | 0666);

// 客户端访问同一内存
key_t client_key = ftok("/etc/app_config", 123);   // 相同参数生成相同key
int client_shmid = shmget(client_key, 0, 0);       // size=0表示获取已有段

2. 硬编码常量(简单场景)

#define APP_SHM_KEY 0x1234  // 预定义全局常量

// 进程A
int shmid_A = shmget(APP_SHM_KEY, 1024, IPC_CREAT | 0600);

// 进程B
int shmid_B = shmget(APP_SHM_KEY, 0, 0);  // 通过相同key访问

风险:可能与其他应用冲突(需确保全局唯一性)。

3. 特殊值 IPC_PRIVATE(私有段)

int shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0600);

总结key

作用与本质

生成方式

使用场景

权限与控制

控制标志:

错误处理

常见错误:

关键注意点

4.2shmat函数:连接共享内存到进程地址空间

功能:将共享内存映射到进程的虚拟地址空间,使进程可访问共享数据。

原型:

void *shmat(int shmid, const void *shmaddr, int shmflg);

参数:

shmid:由 shmget 返回的标识符。

shmaddr:指定连接地址:

shmflg:模式标志:

返回值:

4.3shmdt函数:断开共享内存连接

功能:将共享内存段从当前进程的地址空间分离(解除映射),但不会删除共享内存。

原型:

int shmdt(const void *shmaddr);

参数:

返回值:

底层机制:通过 do_munmap() 释放对应的虚拟内存区间。

4.4shmctl函数:控制共享内存

功能:管理共享内存段,包括删除、状态查询或权限修改。

原型:

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数:

shmid:共享内存标识符。

cmd:控制命令:

buf:指向 shmid_ds 结构体的指针(用于输入/输出数据)。

返回值:

4.5 拓展——命令行中如何实现上面系统调用函数相同的效果

1. ipcs 命令:查看共享内存信息(对应函数状态监控)

2. ipcrm 命令:删除共享内存(对应 shmctl(shmid, IPC_RMID, NULL))

功能:

删除指定的共享内存段,效果等同于调用shmctl的IPC_RMID命令(标记删除,当所有进程调用shmdt后实际释放内存)。

语法:

ipcrm -m <SHMID>  # 删除指定SHMID的共享内存

批量删除(根据用户或键值):

# 删除用户alice创建的所有共享内存
ipcs -m | awk '/alice/{print $2}' | xargs -n1 ipcrm -m

# 删除键值为0x12345的共享内存
ipcs -m | awk '/0x12345/{system("ipcrm -m "$2)}'

需注意:删除时若仍有进程挂载(NATTCH > 0),内存不会立即释放,需等待所有进程调用shmdt 。

3. pmap 命令:查看进程挂载的共享内存(对应 shmat 映射)

功能:

显示进程虚拟地址空间中挂载的共享内存区域,相当于查看shmat返回的映射地址 。

语法:

pmap -x <PID>  # 查看指定进程的内存映射

输出示例:

Address    Kbytes   RSS   Mode  Mapping
7f2a1a000000 1024    rw-s  /SYSV00000000  # 共享内存标识(KEY为0x00000000)

4. 挂载/卸载共享内存的替代方法

挂载(模拟 shmat):

命令行无法直接挂载共享内存到进程空间,但可通过调试器临时操作:

gdb -p <PID> -ex "call shmat(<SHMID>, NULL, 0)" --batch

此操作需进程主动配合,仅用于调试 。

卸载(模拟 shmdt):

同样需在进程内部触发,无直接命令替代。可通过终止进程自动卸载(进程退出时会自动调用shmdt)。

对比总结:函数与命令行操作对应关系

函数功能命令行工具关键参数/操作限制说明
创建共享内存 (shmget)无直接替代–需编程实现
查看共享内存状态 (IPC_STAT)ipcs -m-i <SHMID> 查看详情信息只读,不可修改
删除共享内存 (IPC_RMID)ipcrm -m <SHMID>需指定SHMID需root或所有者权限
查看进程映射 (shmat地址)pmap -x <PID>过滤/SYSV字段仅显示地址,无法主动挂载
卸载共享内存 (shmdt)终止进程kill <PID>进程退出时自动卸载

4.6 shmid和key的区别

1. 核心定义与功能层级

概念功能描述层级归属类比关系
key由 ftok() 生成或用户指定的整数值,用于在系统层面唯一标识共享内存段,内核通过 key 区分不同共享内存内核层标识符类似文件的 inode 号(唯一标识文件)
shmid由 shmget() 系统调用返回的整数值,作为用户层操作共享内存的句柄,用于后续关联、去关联或控制操作用户层标识符类似文件的 文件描述符 fd(用户操作接口)

 关键引用:

2. 生成与使用场景对比

(1) key 的生成与作用

生成方式:

核心作用:

(2) shmid 的生成与作用

由 shmget(key, size, flags) 返回(如 int shmid = shmget(key, 4096, IPC_CREAT|0666);)。

核心作用:

作为用户层操作的入口参数,用于:

命令行工具操作:ipcrm -m shmid 删除共享内存 。

注意:

3. 设计目的与架构思想

维度keyshmid
唯一性范围全局唯一(整个操作系统内)进程内有效(不同进程的 shmid 可能不同)
生命周期持久存在,直至共享内存被删除随进程结束失效,但共享内存仍存留
安全隔离内核维护,用户不可直接操作用户直接使用,但无系统级权限
设计目标解耦:内核通过 key 管理资源唯一性封装:为用户提供安全操作接口

 架构意义:

key 解决“资源是谁”的问题(系统唯一标识),shmid 解决“如何操作”的问题(用户接口)。

5. 代码示例

和管道一样,我们也来段代码加深对共享内存通信的理解。

这里我们和命名管道一样,实现让两个毫无关系的进程通信,所以我们将一个进程看作服务端,另一个进程看作客户端,然后进行封装。

我们先来介绍一段宏

宏定义结构

#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)

do { ... } while (0):

这是一种宏定义的惯用技巧,目的是将多条语句封装为单条逻辑块。

反斜杠\:

用于连接多行代码,使宏定义可跨行书写,提高可读性。

核心功能

perror(m):

输出系统错误信息。参数m是自定义的错误提示字符串(如"open error"),实际输出格式为:m: 具体错误原因。

例如:perror("open error") 可能输出 open error: No such file or directory。

exit(EXIT_FAILURE):

立即终止程序,并返回预定义的失败状态码(通常为非0值)。

该宏是C语言中处理系统调用错误的通用模式,通过perror提供清晰的错误诊断,并通过exit(EXIT_FAILURE)确保程序在致命错误时立即终止。其设计兼顾了安全性、可读性和可移植性。

5.1 创建并获取共享内存

我们让服务端创建共享内存段,客户端则获取共享内存段

但是创建共享内存段之前,得先将共享内存段的唯一标识符key生成,也就需要通过ftok函数来生成key,但是要基于文件路径(pathname)和项目ID(proj_id)生成唯一 key。所以我们先定义全局变量pathname和proj_id,pathname为当前路径,proj_id则取66的十六进制,定义全局变量方便我们在服务端和客户端构造函数时通过传参来生成唯一key(注意:需要两者参数相同,才能获得同一个key,内核才能通过key找到同一个共享内存段)。服务端创建好共享内存段后,客户端就不再需要创建了,只需要获取即可,所以我们要实现二者隔离

代码如下:

#pragma once

#include <iostream>
#include <cstdio>
#include <string>
#include <sys/ipc.h>
#include <sys/shm.h>

#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)

const int gdefaultid = -1;
const int gsize = 4096;
const std::string pathname = ".";
const int proj_id = 0x66;

#define CREATER "creater"
#define USER "user"

class Shm
{
private:
    int _shmid;
    int _size;
    key_t _key;
    std::string _usertype;

    void CreatShm(int flg)
    {
        _shmid = shmget(_key, _size, flg);
        if(_shmid < 0)
        {
            ERR_EXIT("shmget");
        }
        printf("shmid: %d\n", _shmid);
    }

    void Creat()
    {
        CreatShm(IPC_CREAT | IPC_EXCL | 0666);
    }

    void Get()
    {
        CreatShm(IPC_CREAT);
    }

public:
    Shm(const std::string& pathneme, int projid, const std::string& usertype)
        : _shmid(gdefaultid), _size(gsize), _usertype(usertype)
    {
        _key = ftok(pathname.c_str(), projid);
        if (_key < 0)
        {
            ERR_EXIT("ftok");
        }
        printf("key: 0x%x\n", _key);

        if(_usertype == CREATER)
        {
            // 用户是服务端则创建共享内存段
            Creat();
        }
        else if(_usertype == USER)
        {
            // 用户是客户端则获取共享内存段
            Get();
        }

    }
    ~Shm() {}
};

5.2将共享内存段附加到进程地址空间

创建好共享内存段之后,就需要将进程地址空间和共享内存段建立连接。这里我们让操作系统来给我们映射到进程的虚拟地址空间(第二个参数为空指针,详细请看上文shmat函数),同时如果shmat连接失败会返回 (void*)-1 ,所以我们要把它强制转换为long long再做判断(注意我们是64位机器,指针是8个字节大小,而int只有4个字节,所以要强制转换为long long),如果shmat成功就会返回共享内存首地址指针(挂接的进程虚拟地址),所以我们再增加一个获取该指针的成员变量,方便我们将地址打印出来查看

代码如下:

    // 新增一个成员变量
    void* _start_mem;// 共享内存首地址指针

    void Attach()
    {
        _start_mem = shmat(_shmid, NULL, 0);
        if((long long)_start_mem < 0)
        {
            ERR_EXIT("shmat");
        }
        printf("attach success\n");
    }

public:
    Shm(const std::string& pathneme, int projid, const std::string& usertype)
        : _shmid(gdefaultid), _size(gsize), _start_mem(nullptr), _usertype(usertype)
    {
        _key = ftok(pathname.c_str(), projid);
        if (_key < 0)
        {
            ERR_EXIT("ftok");
        }
        printf("key: 0x%x\n", _key);

        if(_usertype == CREATER)
        {
            // 用户是服务端则创建共享内存段
            Creat();
        }
        else if(_usertype == USER)
        {
            // 用户是客户端则获取共享内存段
            Get();
        }

        Attach();
    }

    void* VirtualAddr()
    {
        printf("VirtualAddr: %p\n", _start_mem);
        return _start_mem;
    }

当然我们也可以来一个获取共享内存段大小的接口

代码如下:

    int Size()
    {
        return _size;
    }

5.3分离共享内存段

在我们使用共享内存通信完之后,需要将内存进行回收,避免内存泄漏,但在回收共享内存之前,需要将共享内存段从当前进程的地址空间中分离出去。进程不能再通过该指针访问共享内存。注意:分离操作并不会销毁共享内存段本身。

    void Detach()
    {
        int n = shmdt(_start_mem);
        if(n < 0)
        {
            ERR_EXIT("shmdt");
        }
        printf("Detach success\n");
    }

5.4销毁共享内存段

销毁前先将挂接的共享内存分离,然后再销毁,当然谁创建的就由谁来删除

注意:IPC_RMID:标记删除共享内存。当所有进程均断开连接(shmdt)后,内存才会被实际释放

代码如下:

    void Destroy()
    {
        Detach();
        if(_shmid == gdefaultid)
            return;
        if(_usertype == CREATER)
        {
            int n = shmctl(_shmid, IPC_RMID, NULL);
            if(n < 0)
            {
                ERR_EXIT("shmctl");
            }
            printf("shmctl delete shm: %d success!\n", _shmid);
        }
    }

析构时,调用Destroy函数

5.5 测试代码

此时我们就可以使用共享内存,怎么使用呢?多个进程通过它们各自 shmat 返回的指针(指向同一物理内存的不同虚拟地址)直接读写共享内存区域。就如同我们malloc出来的一段内存对这段内存空间进行使用,我们现在就可以使用这段共享内存来读写。

服务端:

服务端读内存中的数据

#include "Shm.hpp"

int main()
{
    Shm shm(pathname, proj_id, CREATER);
    char* mem = (char*)shm.VirtualAddr();
    while(true)
    {
        printf("%s\n", mem);
        sleep(1);
    }
    return 0;
}

客户端:

客户端对共享内存写

#include "Shm.hpp"

int main()
{
    Shm shm(pathname, proj_id, USER);
    char* mem = (char*)shm.VirtualAddr();
    for(char c = 'A'; c <= 'Z'; c++)
    {
        mem[c - 'A'] = c;
        sleep(1);
    }
    return 0;
}

运行结果:

可以看到运行结果正常,进程挂接数也从无到2,不过由于我们的服务端是死循环在读,所以不会自己调用析构函数,我们需要自己通过命令行ipcrm -m [shmid]来删除共享内存,不然下次再运行就会报错文件存在

不过共享内存也同样存在缺点

共享内存的缺点与挑战

  1. 缺乏内置同步: 这是最大的挑战和风险点。 开发者必须严格、正确地使用额外的同步机制(信号量、互斥锁)来协调多个进程对共享内存的并发访问,否则极易导致数据损坏、程序崩溃等难以调试的问题。

  2. 复杂性增加: 相比管道简单的 read/write 接口,共享内存的创建、附加、分离、销毁步骤更多,并且必须手动管理同步,增加了程序的复杂性。

  3. 资源管理: 共享内存段独立于进程存在。如果进程异常终止而没有正确分离或标记删除 (IPC_RMID),共享内存段可能残留在系统中,造成资源泄漏(ipcs 命令查看,ipcrm 命令删除)。需要良好的编程习惯和可能的清理机制。

  4. 安全性: 需要正确设置权限 (shmflg 中的权限位),防止未授权进程访问敏感数据。

  5. 内存模型: 不同进程附加的地址 (shmat 返回值) 可能不同,不能直接传递指针(传递指针在接收进程地址空间无效)。通常传递的是相对于共享内存基址的偏移量。

源码:

Shm.hpp

#pragma once

#include <iostream>
#include <cstdio>
#include <string>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>

#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)

const int gdefaultid = -1;
const int gsize = 4096;
const std::string pathname = ".";
const int proj_id = 0x66;

#define CREATER "creater"
#define USER "user"

class Shm
{
private:
    int _shmid;
    int _size;
    key_t _key;
    std::string _usertype;
    void* _start_mem;// 共享内存首地址指针(挂接后的进程虚拟地址)

    void CreatShm(int flg)
    {
        _shmid = shmget(_key, _size, flg);
        if(_shmid < 0)
        {
            ERR_EXIT("shmget");
        }
        printf("shmid: %d\n", _shmid);
    }

    void Creat()
    {
        CreatShm(IPC_CREAT | IPC_EXCL | 0666);
    }

    void Get()
    {
        CreatShm(IPC_CREAT);
    }

    void Attach()
    {
        _start_mem = shmat(_shmid, nullptr, 0);
        if((long long)_start_mem < 0)
        {
            ERR_EXIT("shmat");
        }
        printf("attach success\n");
    }

    void Detach()
    {
        int n = shmdt(_start_mem);
        if(n < 0)
        {
            ERR_EXIT("shmdt");
        }
        printf("Detach success\n");
    }

    void Destroy()
    {
        Detach();
        if(_shmid == gdefaultid)
            return;
        if(_usertype == CREATER)
        {
            int n = shmctl(_shmid, IPC_RMID, NULL);
            if(n < 0)
            {
                ERR_EXIT("shmctl");
            }
            printf("shmctl delete shm: %d success!\n", _shmid);
        }
    }

public:
    Shm(const std::string& pathneme, int projid, const std::string& usertype)
        : _shmid(gdefaultid), _size(gsize), _start_mem(nullptr), _usertype(usertype)
    {
        _key = ftok(pathname.c_str(), projid);
        if (_key < 0)
        {
            ERR_EXIT("ftok");
        }
        printf("key: 0x%x\n", _key);

        if(_usertype == CREATER)
        {
            // 用户是服务端则创建共享内存段
            Creat();
        }
        else if(_usertype == USER)
        {
            // 用户是客户端则获取共享内存段
            Get();
        }

        Attach();
    }

    void* VirtualAddr()
    {
        printf("VirtualAddr: %p\n", _start_mem);
        return _start_mem;
    }

    int Size()
    {
        return _size;
    }

    ~Shm() 
    {
        Destroy();
    }
};

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文