【Linux】进程间通信 —— 共享内存

news2024/12/23 19:54:41

文章目录

  • 📕 共享内存的原理
  • 📕 代码实现 & 深入理解共享内存
    • shmget() 函数
    • shmctl() 、shmdt()、shmat()
    • 特点
  • 📕 源代码
    • comm.hpp
    • server.cc
    • client.cc

📕 共享内存的原理

我们知道,如果想实现进程间通信,那么必须要让两个进程看到同一份资源。匿名管道、命名管道 可以实现进程间通信。但是其涉及到文件的创建,所以速度上慢了些。而共享内存就没有这种问题,它是让两个进程直接看到同一份物理内存空间,这样就可以实现进程间通信!!

如下,如果有一种接口,可以在物理内存中开辟出一块空间。
在这里插入图片描述

其次,在进程A中,通过页表,将之前在物理空间开辟的那块空间的地址,映射到A进程的地址空间的共享区中的某个区域,然后就可以将其返回给用户。这样,用户就可以通过进程 A 的共享区,进而访问到物理内存开辟的空间。
进程B同理。这样子,进程A和进程B,就可以看到物理内存中同一块空间,具备了进程间通信的条件!如下图。

而进程 A、B 看到的物理内存中的同一块空间,就是共享内存!!

在这里插入图片描述

管道是让两个进程看到同一个文件,而共享内存是让两个进程看到同一块物理地址,清楚两者的差别!

当不需要进行进程间通信的时候,只需要通过页表,将进程的虚拟地址和物理地址(共享内存)之间的映射关系去掉,然后释放共享内存块,就可以了!

当然,这只是一个宏观上的感知, 要深入理解,必然是要通过写代码的方式!!

📕 代码实现 & 深入理解共享内存

shmget() 函数

申请共享内存块,需要通过 shmget() 函数实现,如下是其介绍。

shmget()
功能:用来创建共享内存
原型
int shmget ( key_t key, size_t size, int shmflg);
参数
key:这个共享内存段名字
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的(位图结构)
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1

对于该函数第一个参数 key ,它随便怎么设置都可以,只要是一个唯一标识即可,但是一般而言,不会随便传入参数,而是通过另一个函数 ftok() 来得到。
为什么要确保 key 的唯一性呢?这是因为,在操作系统中,不一定 只有一对进程 在进行 进程间通信,可能有多对进程同时通信,每一对通信的进程(假设都使用共享内存方式),都要创建新的共享内存块来维持其通信。那么系统中一定同时存在大量的共享内存,那么操作系统就需要管理它们!当然是先描述、再组织

所以,共享内存 并不是 只需要在内存中开辟空间那么简单,操作系统还要为共享内存创建结构体,里面存放的是共享内存的属性。

那么,共享内存 = 内核数据结构(伪代码 struct shm) + 物理内存开辟的空间

如下,进程 A 创建一块共享内存(红色的),然后进程 B 想要访问这块共享内存,和进程A实现通信。那么,进程 B 就要遍历内存中的 struct shm 对象,找到进程 A 创建的,然后通过该对象,找到共享内存。
在这里插入图片描述

如下,有了 ftok() 创建出的唯一标识,就可以在多个 struct shm 结构体对象中,A进程创建的,从而实现A、B进程的进程间通信。

进程 A 在创建共享内存的时候,调用的 shmget() 函数的第一个参数 key,这是通过 ftok() 函数唯一生成的一个标识,进程A创建共享内存的时候,会把这个唯一标识放到对应的 struct shm 对象里面,进程 B 只需要知道 进程A 创建的 key 值(只要 ftok() 的两个参数一样,生成的 key 值就一样,所以实际上是知道 pathname 和 proj_id),即可找到进程A创建的共享内存。
在这里插入图片描述


如下是对 ftok() 的介绍。第一个参数是路径,第二个参数是项目的 ID。

在这里插入图片描述
如下,可以直接调用 getkey() 函数获得特定的 key 值。 两个进程可以分别调用该函数,获得同一个 key 值。

#define PATH "."
#define PROJID 0x1111

key_t getkey()
{
    key_t k=ftok(PATH,PROJID);
    if(k == -1)
    {
        cout<<"ftok error:"<<errno<<strerror(errno)<<endl;
        exit(1);
    }
    return k;
}

然后就是使用 shmget() 函数,创建共享内存。

如下是对其封装,第三个参数是位图结构,和文件系统里面的 bitmap 有异曲同工之妙。主要用到 IPC_CREAT 和 IPC_EXCL 。

  • 单独使用IPC_CREAT: 创建一个共享内存,如果共享内存不存在,就创建之,如果已经存在,获取已经存在的共享内存并返回。
  • IPC_EXCL不能单独使用,一般都要配合IPC_CREAT。
  • IPC_CREAT | IPC_EXCL: 创建一个共享内存,如果共享内存不存在,就创建之, 如果已经存在,则立马出错返回 —— 如果创建成功,对应的shm,一定是最新的!

这里设计 CreateShm 和 Getshm 接口的目的也就很清楚了:如果 进程 A 创建共享内存,那么必定是使用 IPC_CREAT | IPC_EXCL ,那么进程 B 就要通过 key 值找到共享内存,根据上面的规则,需要使用 IPC_CREAT ,所以要设计两个接口,一个给创建共享内存的进程,一个给获取共享内存的进程

当然了,创建共享内存要涉及到权限问题,这里让 拥有者、所属组、other 都是具有读写权限。


#define SIZE 4096  // 共享内存的大小


static int tocreateshm(key_t k,int size,int flag)
{
    int shmid=shmget(k,size,flag);
    if(shmid == -1)
    {
        cout<<"shmget error:"<<errno<<strerror(errno)<<endl;
        exit(2);
    }
    return shmid;
}

int CreateShm(key_t k,int size)
{
    umask(0);
    return tocreateshm(k,size,IPC_CREAT | IPC_EXCL | 0666);
}

int GetShm(key_t k,int size)
{
    return tocreateshm(k,size,IPC_CREAT);
}


shmget() 的返回值是 shmid,以后对这块共享内存的一切操作,都是依靠 shmid 的。但是要区分 shmid 和 key ,key 只是用于创建/找到 共享内存,对共享内存进行操作(应用层),是依靠 shmid。

shmctl() 、shmdt()、shmat()

一个进程创建了共享内存,这个进程也无法直接使用该共享内存,因为进程还没有和共享内存关联起来。还需要链接,需要用到 shmat() ,调用该函数之后,进程就和共享内存关联起来,共享内存就可以映射到进程地址空间的共享区, 该函数返回的是 虚拟内存的地址

shmat()
功能:将共享内存段连接到进程地址空间
原型
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数
shmid: 共享内存标识
shmaddr:指定连接的地址(虚拟地址,一般而言我们不知道挂接在哪里,所以设为 nullptr)
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1

如下,可以验证链接这个过程。 使用 ipcs -m 可以查看共享内存,其中 nattch 就代表其链接数。

在这里插入图片描述

链接成功,就可以开始进程间通信啦!通信结束再取消链接。

当进程A、B通信结束,就可以将进程与共享内存去关联,要用到 shmdt() 。

shmdt()
功能:将共享内存段与当前进程脱离
原型
int shmdt(const void *shmaddr);
参数
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段

如下是代码。

char * AttachShm(int shmid)
{
    char* start=(char*)shmat(shmid,nullptr,0);
    return start;
}

void DetachShm(char* start)
{
    int n=shmdt(start);
    assert(n != -1);
    (void)n;
}

shmctl()
功能:用于控制共享内存
原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1

该函数可以用来删除共享内存块。如下是删除接口,只需要传入 shmid 即可,这是 shmget() 的返回值。
删除共享内存的原因是,如果两个进程都运行结束了,但是进程并没有删除共享内存块,共享内存块依然保存在那里,它不会自己删除。会造成资源浪费(共享内存的生命周期随操作系统)。

void DelShm(int shmid)
{
    int n=shmctl(shmid,IPC_RMID,nullptr);
    assert(n != -1);
    (void)n;
}

特点

  • 共享内存的内存分配是按照 PAGE 为单位的。
  • 进程链接上共享内存之后,不需要额外的接口,就可以直接通信。(管道需要 write、read )
  • 共享内存没有任何保护机制(同步互斥)

如下,画个图简单理解。
左边代表管道, c 代表客户端,s 代表 服务器端,两个进程通信(假设 c 写、s读),c 要先把数据放到自己的缓冲区,然后拷贝到内核,再从内核拷贝到 s 自己的缓冲区。
右边代表共享内存, c 直接把数据放到共享内存,然后 s 就可以直接看到,不需要多次拷贝,所以速度要快很多!!

共享内存的这种特性,使得它是所有进程间通信方案里面,速度最快的。

在这里插入图片描述

📕 源代码

当然了,使用的时候可以封装成为一个类,这样用起来就更简单了!!

comm.hpp

#ifndef __COMM_HPP__
#define __COMM_HPP

#include<iostream>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>
#include<cerrno>
#include<cstring>
#include<sys/stat.h>
#include<cassert>
#include<unistd.h>

using namespace std;

#define PATH "."
#define PROJID 0x1111
#define SIZE 4096
#define CLIENT 0
#define SERVER 1


key_t getkey()
{
    key_t k=ftok(PATH,PROJID);
    if(k == -1)
    {
        cout<<"ftok error:"<<errno<<strerror(errno)<<endl;
        exit(1);
    }
    return k;
}

static int tocreateshm(key_t k,int size,int flag)
{
    int shmid=shmget(k,size,flag);
    if(shmid == -1)
    {
        cout<<"shmget error:"<<errno<<strerror(errno)<<endl;
        exit(2);
    }
    return shmid;
}

int CreateShm(key_t k,int size)
{
    umask(0);
    return tocreateshm(k,size,IPC_CREAT | IPC_EXCL | 0666);
}

int GetShm(key_t k,int size)
{
    return tocreateshm(k,size,IPC_CREAT);
}

void DelShm(int shmid)
{
    int n=shmctl(shmid,IPC_RMID,nullptr);
    assert(n != -1);
    (void)n;
}

char * AttachShm(int shmid)
{
    char* start=(char*)shmat(shmid,nullptr,0);
    return start;
}

void DetachShm(char* start)
{
    int n=shmdt(start);
    assert(n != -1);
    (void)n;
}

class Init
{
public:
    Init(int t)
        :type(t)
    {
        key_t key=getkey();  // 创建 key 值
        if(type == SERVER)   
              shmid=CreateShm(key,SIZE); // 服务器端创建共享内存
        else  shmid=GetShm(key,SIZE);    // 用户端使用共享内存
        start=AttachShm(shmid);          // 关联
    }

    char* getstart()
    {
        return start;
    }

    ~Init()
    {
        DetachShm(start);   // 去关联
        if(type == SERVER) DelShm(shmid);  // 服务器端删除共享内存
    }

private:
    int type;
    char* start;
    int shmid;
};



#endif // !

server.cc

#include"comm.hpp"

int main()
{
    Init init(SERVER);
    char* start=init.getstart();

    int n = 0;
    while (n <= 30)
    {
        cout << "client -> server# " << start << endl;
        sleep(1);
        n++;
    }  

    return 0;
}

client.cc

#include"comm.hpp"

int main()
{
    Init init(CLIENT);
    char *start = init.getstart();

    char c = 'A';

    while (c <= 'Z')
    {
        start[c - 'A'] = c;
        c++;
        start[c] = '\0';
        sleep(1);
    }
    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/507380.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux Shell 实现一键部署subversion

subversion SVN是subversion的缩写&#xff0c;是一个开放源代码的版本控制系统&#xff0c;通过采用分支管理系统的高效管理&#xff0c;简而言之就是用于多个人共同开发同一个项目&#xff0c;实现共享资源&#xff0c;实现最终集中式的管理。 TortoiseSVN TortoiseSVN 是…

C语言函数大全-- t 开头的函数

C语言函数大全 本篇介绍C语言函数大全-- t 开头的函数 1. tan&#xff0c;tanf&#xff0c;tanl 1.1 函数说明 函数声明函数功能double tan(double x)计算 以弧度 x 为单位的角度的正切值&#xff08;double&#xff09;float tanf(float x)计算 以弧度 x 为单位的角度的正…

Spring Boot项目创建和使用

一、Spring Boot简介 1.概念 Spring Boot 就是 Spring 框架的脚⼿架&#xff0c;它就是为了快速开发 Spring 框架⽽诞⽣的。 2.优点 有快速集成框架&#xff0c;可以快速添加外部jar包内置web框架&#xff0c;可以直接运行可以快速部署&#xff0c;不依赖任何外部的web容器…

【牛客刷题专栏】0x26:JZ25 合并两个排序的链表(C语言编程题)

前言 个人推荐在牛客网刷题(点击可以跳转)&#xff0c;它登陆后会保存刷题记录进度&#xff0c;重新登录时写过的题目代码不会丢失。个人刷题练习系列专栏&#xff1a;个人CSDN牛客刷题专栏。 题目来自&#xff1a;牛客/题库 / 在线编程 / 剑指offer&#xff1a; 目录 前言问…

MATLAB 之 数值数据,矩阵的表示和变量及其操作

文章目录 一、数值数据1. 数值数据类型的分类1.1 整型1.2 浮点型1.3 复型 2. 数据的输出格式 二、矩阵的表示1. 矩阵的建立1.1 直接输入法建立矩阵1.2 已建好的矩阵建立更大的矩阵 2. 冒号表达式3. 矩阵元素的引用3.1 矩阵元素的引用方式3.2 利用冒号表达式获得子矩阵3.3 利用空…

Spring执行流程Bean生命周期

Spring执行流程 说明&#xff1a;这里只是说的大概流程&#xff0c;不是严格按照源码上一步一步说的。 简单来说&#xff0c;分为下边四个步骤&#xff1a; 启动Spring容器实例化Bean&#xff08;分配内存空间&#xff09;将Bean注册到Spring当中&#xff08;存操作&#xf…

【常用 Linux 命令的基本使用】总结篇(附必要的 Ubuntu 截图)

本文目录 1. 常用 Linux 命令的基本使用1.1 学习 Linux 终端命令的原因1.2 常用 Linux 命令的基本使用1.3 自动补全 2. Linux 终端命令格式2.1 终端命令格式2.2 查阅命令帮助信息&#xff08;了解&#xff09; 3. 文件和目录常用命令3.1 查看目录内容3.1.1 终端实用技巧3.1.2 l…

shell数组

目录 一&#xff1a;数组定义方法 1、方法一 ​ 2、方法二 ​3、方法三 ​4、方法四 5、判断数组是否完整 &#xff08;1&#xff09;方法一 &#xff08;2&#xff09;方法二&#xff1a;通过脚本 二&#xff1a;获取数组值 1、获取数组长度 2、获取数组数据列表 3、…

本地部署 Stable Diffusion web UI

本地部署 ChatGLM-6B 0. 什么是 Stable Diffusion1. 什么是 Stable Diffusion web UI2. Github 地址3. 安装 Miniconda34. 创建虚拟环境5. 安装 Stable Diffusion web UI6. 启动 Stable Diffusion web UI7. 访问 Stable Diffusion web UI8. 其他 0. 什么是 Stable Diffusion S…

UE4 面试题整理

1、new与malloc的区别 new&#xff1a; new首先会去调用operator new函数&#xff0c;申请足够的内存&#xff08;大多数底层用malloc实现&#xff09;&#xff0c;然后调用类型的构造函数来初始化变量&#xff0c;最后返回自定义类型的指针&#xff0c;delete先调用析构函数&…

IDEA配置使用Git

使用IDEA2018.2.8版本下操作 文章目录 一、安装Git1.GitHub与Git的关系2.Git的安装 二、在IDEA中配置Git三、创建远程仓库和本地仓库1.在gitee上创建远程仓库2.在IDEA上创建本地仓库 四、分支操作1.创建分支2.删除分支3.比较分支4.合并分支 一、安装Git 1.GitHub与Git的关系 …

位运算(算法)

目录 一、位运算的知识点二、位运算的应用1. x & 1一个数补码表示中的第k位数字求补码中含有1的个数偶数位与奇数位分开输出 2. x & (-x)返回x的最后一位1及之后的数字判断一个数是否是2的n次方求补码中含有1的个数 3. x & (x - 1)求补码中含有1的个数&#xff08;…

Transformer模型各模块详解及代码实现

Transformer 前言1、Transformer模型整体架构2、Embeeding2.1 词向量2.1.1 独热编码2.1.2 Word Embedding2.1.3 总结 2.2 代码实现 3、Positional Encoding3.1 位置编码简介3.2 代码讲解 4、Multi-Head Attention5、Layer Norm6、Positionwise Feed Forward7、Encoder and Deco…

Qt Creator 的使用技巧

1、Qt Creator 的快捷键 在 Qt Creator 里&#xff0c;假若自己不知道某些功能按钮的快捷键是什么&#xff0c;可以将鼠标移至该按钮 上面就可以知道它的快捷键了。如下图&#xff0c;想知道运行的快捷键是什么&#xff0c;那么我们将鼠标移至 Qt Creator 的左下角的运行…

日常开发中代码技巧(个人使用)

日常开发中代码技巧 String&#xff0c;StringBuffer判断非空 String判断非空 参考&#xff1a;https://blog.csdn.net/Echo_width/article/details/79653704 首先&#xff0c;区分空串和null串 1 空串""是长度为0的字符串&#xff0c;它有自己的串长度&#xff08;…

Python每日一练:小艺读书醉酒的狱卒非降序数组(详解快排)

文章目录 前言一、小艺读书二、醉酒的狱卒三、非降序数组总结 前言 今天这个非降序数组&#xff0c;阅读解理小学水平&#xff0c;说起来都是泪啊。我折腾了一天都没搞定&#xff0c;从冒泡写到快速排序。换了几种都还不行&#xff0c;我又给快排加上插入排序。结果还是不能全…

MySQL--复合查询--0422

注&#xff1a;为了方便查看 mysql语句会有突然的换行&#xff0c;书写时请勿模仿。 目录 1.单表查询回顾 显示工资最高的员工的名字和工作岗位 显示工资高于平均工资的员工信息 2.多表查询 比如需要查询雇员名字和所在部门编号及部门名字。 显示部门号为10的部门名&…

“智慧赋能 强链塑链”——精细化工行业仓储物流数字化转型探讨

精细化工行业作为衡量国家化学工业水平高低的重要标志&#xff0c;为国民经济提供重要的终端产品支持&#xff0c;相比较大化工产品&#xff0c;精细化工产品需要高度专业技能和工艺&#xff0c;其生产过程需要复杂的化学反应&#xff0c;以及严格的控制条件&#xff0c;产出的…

基于 TiDB + Flink 实现的滑动窗口实时累计指标算法

作者&#xff1a;李文杰 前言 在不少的支付分析场景里&#xff0c;大部分累计值指标可以通过 Tn 的方式计算得到 。随着行业大环境由增量市场转为存量市场&#xff0c;产品的运营要求更加精细化、更快速反应&#xff0c;这对各项数据指标的实时性要求已经越来越高。产品如果能…

UDS-19服务的状态掩码字节解析

19服务读DTC信息有01、02、04、06、0A等子服务&#xff0c;使用诊断仪读取DUT数据一般只需发送4字节有效数据&#xff0c;其他无效字节用00或者CC填充&#xff0c;如03 19 02 09 00 00 00 00&#xff0c; 数据分解&#xff1a; 03&#xff1a;表示报文类型未单帧&#xff0c;长…