当前位置: 首页 > news >正文

<进程间通信>共享内存

目录

共享内存工作原理

共享内存数据结构

共享内存使用

代码演示

查看共享内存

 进程关联共享内存

进程去共享内存关联

共享内存释放

指令释放

通过系统控制函数释放

共享内存删除机制

共享内存快的原因&

共享内存的缺点


前言

  • System V 是一套独立于操作系统外的标准,是一个专门为了通信设计出的内核模块,我们称之为 System V 的 IPC 通信机制。共享内存 全称 System V  共享内存,是一种进程间通信解决方案,并且是所有解决方案中最快的一个,在通信速度上可以做到一骑绝尘。

共享内存工作原理

  • 在物理内存中开辟一块公共内存区域,让两个不同的进程的虚拟地址同时对此建立映射关系,此时两个独立的进程能看到同一块空间。进程地址空间映射完毕以后返回首个虚拟地址,以便于进程之间进行通信,可以直接对此空间进【写入或读取】,这块公共区域就是 共享内存。

  • 共享内存的目的也是让不同的进程看到同一份资源(同一个内存)

备注:

  • 共享区作为虚拟地址空间中一块缓冲区域,既可作为堆栈生长扩展的区域,也可用来存储各种进程间的公共资源,比如这里的共享内存,以及之前学习的动态库,相关信息都是存储在共享区中
  • 共享内存块的创建、进程间建立映射都是由 OS 实际执行的

共享内存数据结构

  • 共享内存不止存在一份,当出现多块共享内存时,操作系统不可能一一比对进行使用,秉持着高效的原则,操作系统会把已经创建的共享内存组织起来,更好的进行管理。
  • 共享内存不是只要在内存中开辟空间即可,系统也要为了管理共享内存,构建对应的描述共享内存的结构体对象! 在Linux中这个结构体是struct shmid_ds,这个结构体里面存放了共享内存的各种属性。
  • 共享内存 = 共享内存的内核数据结构(struct shmid_ds) + 真正开辟的内存空间
  • 共享内存不止用于两个进程间通信,所以共享内存必须确保能持续存在,这也就意味着共享内存的生命周期不随进程,而是随操作系统,一旦共享内存被创建,除非被删除,否则将会一直存在,因此 操作系统需要对共享内存的状态加以描述。
shstructmid_ds
{
    struct ipc_perm shm_perm;    /* 操作权限 */
    int shm_segsz;               /* 段的大小(字节) */
    __kernel_time_t shm_atime;   /* 最后附加时间 */
    __kernel_time_t shm_dtime;   /* 最后分离时间 */
    __kernel_time_t shm_ctime;   /* 最后修改时间 */
    __kernel_ipc_pid_t shm_cpid; /* 创建者的进程 ID */
    __kernel_ipc_pid_t shm_lpid; /* 最后操作者的进程 ID */
    unsigned short shm_nattch;   /* 当前附加的进程数 */
    unsigned short shm_unused;   /* 兼容性字段 */
    void *shm_unused2;           /* 兼容性字段,用于 DIPC */
    void *shm_unused3;           /* 未使用的字段 */
};
  •  struct ipc_perm 中存储了 共享内存中的基本信息
struct ipc_perm
{
    __kernel_key_t key;       // IPC 对象的唯一标识符
    __kernel_uid_t uid;       // 当前拥有者的用户 ID
    __kernel_gid_t gid;       // 当前拥有者的组 ID
    __kernel_uid_t cuid;      // 创建者的用户 ID
    __kernel_gid_t cgid;      // 创建者的组 ID
    __kernel_mode_t mode;     // 访问权限模式
    unsigned short seq;       // 序列号,用于区分同 key 的不同 IPC 对象
};

共享内存使用

函数返回值参数备注
int shmget(key_t key,size_t size,int shmflg);创建成功返回共享内存的 shmid,失败返回 -1

参数1 key_t key    创建共享内存的唯一 key 值,通过 ftok函数获取


参数2 size_t size    创建共享内存的大小,一般为 4096


参数3 int shmflg     位图,可以设置共享内存的创建方式及创建权限

因为共享内存拥有自己的数据结构,所以返回值 int 实际就是 shmid,类似于文件系统中的 fd,用来对不同的共享内存块进行操作

参数2为创建共享内存的大小,单位是字节,一般设为 4096 字节(4kb),与一个 PAGE 页大小相同,有利于提高IO效率

参数3是位图结构,类似于 open 函数中的参数3(文件打开方式),常用的标志位有以下几个:

  • IPC_CREAT :创建共享内存,如果存在,则使用已经存在的

  • IPC_EXCL :避免使用已存在的共享内存,不能单独使用,需要配合 IPC_CREAT 使用,作用是当创建共享内存时,如果共享内存已经存在,则创建失败。通过这两个标志位的组合,我们能够保证我们拿到的共享内存一定是最新的,而不是以前其他进程可能使用过的

  • mode_flags:因为共享内存也是文件,所以权限可设为文件的起始权限 0666
key_t ftok(const char* pathname,int proj_id);返回生成的标识值,等价于 int 类型

参数1 const char *pathname:项目路径,可使用 绝对 或 相对 路径

参数2 int proj_id:项目编号,可以根据实际情况编写

这个key值单独看起来是没有任何作用的,它要与其他的系统调用结合起来才会发挥作用,未来要通信的两个进程就可以在函数 ftok 输入相同的相同的参数,生成相同的key值,然后通过这个key值就能确定它们想要通信的共享内存是哪一个了

代码演示

comm.hpp

#include <iostream>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
 
using namespace std;
 
#define PATHNAME "."     // 项目名
#define PROJID 0x0001    // 项目编号
 
const int gsize = 4096;
const mode_t mode = 0666;
 
// 将十进制转换成十六进制
string toHex(int x)
{
  char buffer[64];
  snprintf(buffer, sizeof(buffer), "0x%x", x);
  return buffer;
}
 
// 获取共享内存唯一标识
key_t getKey()
{
  key_t key = ftok(PATHNAME, PROJID);
 
  if (key == -1)
  {
    cerr << errno << ":" << strerror(errno) << endl;
    exit(1);
  }
  return key;
}
 
// 共享内存助手
int createShmHelper(key_t key, int size, int flag)
{
  int shmid = shmget(key, size, flag);
  if(shmid == -1)
  {
    cerr << errno << ":" << strerror(errno) << endl;
    exit(2);
  }
  return shmid;
}
 
// 创建共享内存
int createShm(key_t key, int size)
{
  return createShmHelper(key, size, IPC_CREAT | IPC_EXCL | mode);
}
 
// 获取共享内存
int getShm(key_t key, int size)
{
  return createShmHelper(key, size, IPC_CREAT);
}

server.cc

#include "common.hpp"
 
int main()
{
  // 创建key
  key_t key = getKey();
  cout << "server key:  " << toHex(key) <<  endl;
 
  // 服务端创建共享内存
  int shmid = createShm(key, gsize);
  cout << "server shmid: " << shmid << endl;
  return 0;
}

client.cc

#include "common.hpp"
 
int main()
{
  key_t key = getKey();
  cout << "client key:  " << toHex(key) << endl;
 
  // 客户端打开内存
  int shmid = getShm(key, gsize);
  cout << "client shmid: " << shmid << endl;
 
  return 0;
}

演示结果:

  • 通过shmget和ftok函数可以获取唯一的key和shmid

查看共享内存

ipcs -m

备注:

  • 从左到右依次为key值、 shmid、拥有者、权限、大小、挂接数、状态
  • 共享内存 0 就是通过上述代码生成的
  • 因为共享内存每次都是随机生成的,所以每次生成的 key 和 shmid 都不一样

再次运行上面代码:

解析:

  • 服务端运行失败,原因是 shmget 创建共享内存失败,这是因为服务端创建共享内存时,传递的参数为 IPC_CREAT | IPC_EXCL,其中 IPC_EXCL 注定了当共享内存存在时,创建失败。
  • 而客户端只是单纯的获取共享内存,同时也只传递了 IPC_CREAT 参数,所以运行才会成功。
  • 综上所述,服务端运行失败的根本原因是 待创建的共享内存已存在,如果想要成功运行,需要先将原共享内存释放

 进程关联共享内存

函数返回值参数

void* shmat(int shmid,const void* shmaffr,int shmflg)

一般为:shmat(shmid,NULL,0);

返回值 void*,如同malloc一样,返回的是 void* 指针,可以根据需求进行强转

当进程与共享内存关联后,返回的就是共享内存映射至共享区的起始地址

  • 关联成功返回起始地址
  • 关联失败返回 (void*) - 1

一般通信数据为字符,所以可以将 shmat 的返回值强转为 char*

参数1 int shmid    待关联的共享内存id


参数2 const void *shmaddr    
共享内存挂接成功后得到的共享内存的虚拟地址的起始地址,一般设置为 NULL ,让系统自己挂接,可以不用管

  • 共享内存映射至共享区时,我们可以指定映射位置(即传递参数2),但我们一般不知道具体地址,所以 可以传递 NULL,让编译器自动选择位置进行映射

参数3 int shmflg  关联后,进程对共享内存的读写属性,一般直接设为 0,表示关联后,共享内存属性为 默认读写权限,更多选项如下所示(了解):

  • SHM_RDONLY关联共享内存后只进行读取操作
  • SHM_RND 若 shmaddr 不为 NULL,则关联地址自动向下调整为 SHMLBA 的整数倍,SHMLBA 的值为 PAGE_SIZE,具体调整公式:shmaddr - (shmaddr % SHMLBA)

代码演示

comm.hpp添加:

// 关联共享内存
char* attachShm(int shmid)
{
  char *start = (char*)shmat(shmid, nullptr, 0);
  return start;
}

server.cc

#include "common.hpp"
 
int main()
{
  // 创建key
  key_t key = getKey();
  cout << "server key:  " << toHex(key) <<  endl;
 
  // 服务端创建共享内存
  int shmid = createShm(key, gsize);
  cout << "server shmid: " << shmid << endl;
  
  sleep(3);
 
  // 将自己与共享内存关联起来
  char *start = attachShm(shmid);
  sleep(3);
 
 
  sleep(5);
  // 删除共享内存
  shmctl(shmid, IPC_RMID, NULL);
  cout << "共享内存" << shmid << "已删除" << endl;
  return 0;
}

client.cc

#include "common.hpp"
 
int main()
{
  key_t key = getKey();
  cout << "client key:  " << toHex(key) << endl;
 
  int shmid = getShm(key, gsize);
  cout << "client shmid: " << shmid << endl;
  
  sleep(3);
 
  // 将自己与共享内存关联起来
  char *start = attachShm(shmid);
  sleep(5);
 
  return 0;
}

演示结果:

备注:

  • 共享内存信息中的 nattch 表示当前共享内存中的进程关联数 。注意: 程序运行结束后,会自动取消关联状态
  • 共享内存的生命周期是随内核的,即进程退出以后如果没有删除共享内存,则共享内存不会消失!

进程去共享内存关联

函数返回值参数备注
int shmdt(const void* shmaddr);

去关联成功返回 0

失败返回 -1,并将错误码设置

参数:关联成功的返回共享内存起始地址

如同关闭 FILE*、fd、free 等一些列操作一样,当我们关联共享内存,使用结束后,需要进行去关联,否则会造成内存泄漏(指针指向共享内存,访问数据)

共享内存释放

指令释放

ipcrm -m shmid

通过系统控制函数释放

函数返回值参数备注
int shmctl(int shmid,int op,struct shmid_ds* buf);

 成功返回 0

 失败返回 -1

参数1 int shmid    待控制的共享内存 id


参数2 int cmd    控制共享内存的具体动作,同样是位图


参数3 struct shmid_ds *buf    用于获取或设置所控制共享内存的具体数据结构,shmid_ds结构中,调用者必须要有读权限

参数2:

  • IPC_RMID,表示释放共享内存,调用者必须是共享内存的创建者,或者是特权用户,后面参数3跟着NULL,释放共享内存
  • IPC_STAT用于获取共享内存的数据结构,可以获取共享内存段的大小、创建者、最后操作者、挂接次数等信息
  • IPC_SET 在进程有足够权限的前提下,将共享内存的当前关联值设置为 buf 数据结构中的值,以修改共享内存段的权限(如读写权限)、所有者(uidgid)等

buf 就是共享内存的数据结构,可以使用 IPC_STAT 获取,也可以使用 IPC_SET 设置

在 Linux 系统中,使用 IPC_SET 修改共享内存段的属性(如权限、所有者等)时,确实需要一定的权限。这里的“足够权限”通常指的是以下两种情况:

1. 创建者权限

如果调用进程是共享内存段的创建者,那么它可以直接使用 IPC_SET 修改该共享内存段的属性。创建者是指通过 shmget() 创建共享内存段的进程,其用户 ID 和组 ID 被记录在 struct shmid_dscuidcgid 字段中。

  • 示例:如果一个进程以用户 user1 的身份创建了共享内存段,那么该进程(或其子进程)可以使用 IPC_SET 修改该段的权限或所有者。

2. 特权用户(如 root)

如果调用进程的用户是特权用户(如 root),那么它也可以使用 IPC_SET 修改共享内存段的属性,即使它不是该段的创建者。特权用户通常是指具有超级用户权限(UID = 0)的用户。

  • 特权用户root 用户(UID = 0)或其他具有 CAP_SYS_ADMINCAP_IPC_OWNER 等能力(capabilities)的用户。

  • 示例:如果一个普通用户创建了共享内存段,root 用户可以使用 IPC_SET 修改该段的权限或所有者,即使它不是创建者。

server.cc

#include "common.hpp"
 
int main()
{
  // 创建key
  key_t key = getKey();
  cout << "server key:  " << toHex(key) <<  endl;
 
  // 服务端创建共享内存
  int shmid = createShm(key, gsize);
  cout << "server shmid: " << shmid << endl;
 
  int n = 5;
  while (n)
  {
    // 运行五秒后删除共享内存
    cout << n-- << endl;
    sleep(1);
  }
 
  // 删除共享内存
  shmctl(shmid, IPC_RMID, NULL);
  cout << "共享内存" << shmid << "已删除" << endl;
  return 0;
}

当服务端运行结束时,会自动删除共享内存:

共享内存删除机制

  • 当使用 shmctl(shmid, IPC_RMID, 0) 删除共享内存时,操作系统会检查当前是否有进程仍然挂接(附加)到该共享内存段。如果存在挂接的进程,共享内存段不会立即被销毁,而是被标记为“销毁”状态(dest),并等待所有挂接的进程分离。

备注:

  • 共享内存在被删除后,已成功挂接的进程仍然可以进行正常通信,不过此时无法再挂接其他进程

  • 共享内存被提前删除后,状态 status 变为 销毁 dest

状态 dest 的含义

  • ipcs 命令的输出中,dest 状态表示共享内存段已被标记为销毁,但仍然存在,直到所有挂接的进程完成分离。此时,共享内存段的 mode 字段会被设置为 SHM_DEST,表示该段即将被销毁。

为何已挂接的进程仍能通信

  • 已挂接的进程仍然可以访问共享内存,因为它们的虚拟地址空间已经映射到共享内存段。即使共享内存被标记为销毁,这些进程仍然可以通过映射的地址访问内存内容,直到它们调用 shmdt() 分离共享内存。

新进程无法挂接的原因

  • 一旦共享内存被标记为销毁(dest),新的进程将无法再通过 shmat() 挂接到该共享内存段。这是因为操作系统已经将其视为即将销毁的资源,不允许新的挂接操作。

共享内存快的原因

共享内存通信快的秘籍在于 减少数据拷贝(IO),IO是很慢、很影响效率的

比如在使用管道通信时,需要经过以下几个步骤:

  • 从进程 A 中读取数据(IO)
  • 打开管道,然后通过系统调用将数据写入管道(IO)
  • 通过系统调用从管道读取数据(IO)
  • 将读取到的数据输出至进程 B(IO)

但共享内存就不一样,直接访问同一块内存进行数据读写。

在使用共享内存通信时,只需要经过以下两步:

  1. 进程 A 直接将数据写入共享内存中
  2. 进程 B 直接从共享内存中读取数据

显然,使用共享内存只需要经过 2 次 IO

所以共享内存的秘籍是 减少拷贝(IO)次数

  • 得益于共享内存的这种特性,可以让进程通信的时候,减少拷贝次数,所以共享内存是所有进程通信中,速度最快的。

共享内存的缺点

共享内存这么快,为什么不直接只使用共享内存呢?

因为快是要付出代价的,因为 “快” 导致共享内存有以下缺点:

  • 多个进程无限制地访问同一块内存区域,导致共享内存中的数据无法确保安全
  • 即 共享内存 没有同步和互斥机制,某个进程可能数据还没写完,就被别人读走了,或者被别人覆盖了

总的来说,共享内存没有任何的保护机制,不加规则限制的共享内存是不推荐使用的。

当然可以利用其他通信方式,控制共享内存的写入与读取规则

  • 比如使用命名管道,进程 A 写完数据后,才通知进程B,读取进程 B 读取后,才通知进程 A 写入
  • 假如是多端写入、多端读取的场景,则可以引入生产者消费者模型,加入互斥锁和条件变量等待工具,控制内存块的读写。

相关文章:

  • 知微传感3D相机上位机DkamViewer使用:设置相机的静态IP
  • 量子计算:商业化应用的未来蓝图
  • linux server docker 拉取镜像速度太慢或者超时的问题处理记录
  • 双指针刷题和总结
  • C# OnnxRuntime部署DAMO-YOLO香烟检测
  • 安全运维:从防火墙到入侵检测
  • 测试工程师Ai应用实战指南简例prompt
  • EX_25/3/3
  • 基于YALMIP和cplex工具箱的IEEE33微电网故障处理电网重构matlab模拟与仿真
  • 国产NAS系统飞牛云fnOS深度体验:从运维面板到博客生态全打通
  • 【新人系列】Golang 入门(二):基本数据类型
  • graido学习记录
  • 【图论】判断图中有环的两种方法及实现
  • vi的常见操作命令
  • [数据结构] - - - 链表
  • 面试题02.01.移除重复节点
  • 【计算机网络03】网络层协议IP(详细)
  • 苹果的 AI 紧急情况
  • KMP算法!
  • Linux 基础---sudo权限 修改文件所属人、用户所属组
  • 贵州省总工会党组成员、副主席梁伟接受审查调查
  • 人民日报刊文:守护“技术进步须服务于人性温暖”的文明底线
  • 本周看啥|喜欢二次元的观众,去电影院吧
  • 保利42.41亿元竞得上海杨浦东外滩一地块,成交楼面单价超8万元
  • 乌克兰议会批准美乌矿产协议
  • 乡村快递取件“跑腿费”屡禁不止?云南元江县公布举报电话