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

文章目录

    • 引入
    • 共享内存的原理
    • 共享内存的相关接口
      • shmget()
      • shmat()
      • shmdt()
      • shmctl()
    • 共享内存的简单使用
    • 共享内存的特点

引入

进程间通信,顾名思义就是一个进程和另一个进程之间进行对话,以此完成数据传输、资源共享、通知事件或进程控制等。

众所周知,进程具有独立性,即使是父子进程也会彼此独立,互相看不到对方的任何信息。

而独立性是阻碍通信的,所以进程间通信要打破这种阻碍,打破进程独立性,也就是要让两个不想干的进程看到同一份资源。

在上一篇文章中我们分别介绍了匿名管道和命名管道两种通信方式,通过建立一个内存级文件,一个进程向该文件写内容,另一个进程从该文件中读内容,这样就完成了两个进程之间的通信。

既然可以建立一个内存级文件,能否省去文件,直接开辟一块内存,要通信的进程能同时使用这块内存呢?

这就是下面要介绍的共享内存(Shared Memory)。


共享内存的原理

在介绍共享内存之前先了解一下它的工作原理。

我们肯定了解C语言中有malloc函数,可以开辟一块内存空间,这块空间能通过页表映射到进程地址空间的堆区。但是进程空间不止有堆区,还有什么栈区、初始化数据区、未初始化数据区等等…其中在堆区和栈区之间还有一个共享区。我们今天要讲的共享内存,就是要映射到共享区的。

我们同样可以使用类似于malloc功能的接口来申请一块内存空间。然后将申请的这部分内存空间映射到进程地址空间的共享区,在另一个进程中我们也做相同的事情,这样两个进程就同时关联了同一块物理内存空间,一个进程向这段内存中写,另一个进程从这块内存读取,这样同样也能实现两个进程之间的通信。当两个进程之间不再需要通信时,我们先不急着释放这块内存,因为释放之后所有共享这块内存的进程都无法使用了,我们也不知道究竟有几个进程同时使用这块内存通信。所以首先取消内存和不再需要通信的进程之间的映射,也就是去关联。当这块内存真不需要的时候,再将其释放掉。

如此就是通过共享内存进行通信的简单原理:
image-20240228100514693


共享内存的相关接口

有了原理层面的简单了解,就可以学习一下相关接口了。下面主要介绍四个接口s。

#include <sys/ipc.h>
#include <sys/shm.h>int shmget(key_t key, size_t size, int shmflg);
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

shmget()

int shmget(key_t key, size_t size, int shmflg);
功能:用来创建共享内存
参数:key: 可以唯一标识共享内存段的key值size: 共享内存块的大小,一般是4kb的整数倍shmflag: 共享内存块的权限,用二进制数中的位进行标识
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1

对于key值,是需要我们设计传过去的,它的类型是key_t,实际是int。key值要保证唯一性,以此保证共享内存可以唯一标识,我们可以通过ftok()函数进行获取,所以再简单介绍一下ftok()函数:

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

很简单,参数为一个字符串和一个整数,字符串一般传我们要创建的共享内存块的路径,proj_id可以设一个随机数,ftok就会生成一个key并返回,如果出错了则返回-1。

size就不多介绍了,还是需要注意size虽然可以随便填,但一般是4kb的整数倍。

shmfalg为创建文件的一些权限选项,其选项常用的主要就两个:IPC_CREATIPC_EXCLIPC_CREAT的作用是如果key对应的共享内存块不存在,则创建一块,然后返回该内存块的shmid;如果已经存在相应的共享内存块了,则直接返回对应的id。而IPC_EXCL 通常要配合IPC_CREAT使用,当要获取的共享内存块不存在时不起作用,当其已经存在时则会获取失败,保证要获取的共享内存块是新鲜的。除此之外还要加上要创建的共享内存块的权限,就跟用open创建文件时的参数mode一样,比如0x0666是所有人可读可写,0x0600是只有自己可读可写。如果忘记加权限则创建出来的共享内存是无法使用的。

shmat()

全称是shm attach,功能就是将进程与共享内存挂接。

void *shmat(int shmid, const void *shmaddr, int shmflg);
功能:将共享内存段连接到进程地址空间
参数:shmid: 共享内存的id,可以唯一标识共享内存shmaddr: 指定链接到进程空间中的地址shmflg: 常用的标识有两个,SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存的首地址,类似于malloc;失败返回-1

对于shmid,这个就是shmget的返回值,和key一样都能进行唯一性标识,区别就是key是内核层的,shmid是应用层的。进程要挂接共享内存时手里肯定得有id。

shmaddr如果传参的话要传一个地址,意思是指定映射到进程地址空间的哪个位置。如果传nullptr则会随机映射,一般都是传nullptr

shmflg如果传了SHM_RDONLY则进程只能从共享内存块中读取。SHM_RND通常要配合shmaddr使用,会使映射的位置不一定在shmaddr处,会向下舍入到SHMLAB的整数倍。用的也很少。如果shmflg传个0过去,则表示可读可写。

shmdt()

全称是shm detach,功能就是取消进程与共享内存的挂接,也就是去关联。

int shmdt(const void *shmaddr);
功能:将共享内存段与当前进程脱离
参数:shmaddr: 由shmat所返回的指针,也就是共享内存的首地址,用法类似于free()
返回值:成功返回0;失败返回-1

该函数的功能仅仅是去关联,并不是删除共享内存段。

当进程不再需要通过该共享内存通信时应及时取消挂接。

shmctl()

全称是shm control,功能就是控制共享内存。

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
功能:用于控制共享内存
参数:shmid: 由shmget返回的共享内存标识码cmd: 将要采取的动作,常用的有三个取值buf: 输出型参数,指向一个存储共享内存块部分属性结构体,如果需要时会将shmid对应的共享内存块的信息拷贝到该结构体中,为输出型参数

cmd的取值主要有IPC_STATIPC_SETIPC_RMID

IPC_STAT的功能是将共享内存内核数据结构中的属性拷贝到buf中,用来开获取共享内存的状态信息,如果要使用这个选项,共享内存必须要可读。

IPC_SET和上面的选项功能相反,是要把buf中的信息写入到内核数据结构中,使用时要谨慎。只有创建或者拥有该共享内存块或被赋予权限的人才能进行此操作。

IPC_RMID就是删除共享内存了,但是只有当没有进程挂接共享内存时该共享内存块才会真正删除。只有创建或者拥有该共享内存块或被赋予权限的人才能进行此操作。一般就是谁创建的谁删除。

bufstruct shmid_ds结构体类型指针,指向这样一个结构,man手册中给出了该结构的部分信息:

struct shmid_ds {struct ipc_perm shm_perm;    /* Ownership and permissions */size_t          shm_segsz;   /* Size of segment (bytes) */time_t          shm_atime;   /* Last attach time */time_t          shm_dtime;   /* Last detach time */time_t          shm_ctime;   /* Last change time */pid_t           shm_cpid;    /* PID of creator */pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */shmatt_t        shm_nattch;  /* No. of current attaches */...
};

其中还包含一个struct ipc_perm shm_perm结构体成员,其信息如下:

struct ipc_perm {key_t          __key;    /* Key supplied to shmget(2) */uid_t          uid;      /* Effective UID of owner */gid_t          gid;      /* Effective GID of owner */uid_t          cuid;     /* Effective UID of creator */gid_t          cgid;     /* Effective GID of creator */unsigned short mode;     /* Permissions + SHM_DEST andSHM_LOCKED flags */unsigned short __seq;    /* Sequence number */
};

共享内存的简单使用

下面举一个使用共享内存进行通信的简单例子。

为了便于使用,我们把一些公共信息比如keypathname和共享内存的接口进行封装一下,放在一个头文件common.hpp中:

// common.hpp#pragma once#include <iostream>
#include <cerrno>		// 用于打印报错信息
#include <cstring>		// 调用C的部分字符串相关的接口
#include <cstdlib>
#include <cstdio>
#include <sys/ipc.h>	// 共享内存的相关接口
#include <sys/shm.h>
using namespace std;#define PATHNAME "." 	// 在当前目录下创建共享内存
#define PROJ_ID	0x66	// ftok()创建key时的参数,任意取值都行
#define MAX_SIZE 4096	// 共享内存的大小,建议4kb的整数倍// 通过ftok()获取key
key_t getKey()			
{key_t k = ftok(PATHNAME, PROJ_ID);if (k < 0){cerr << errno << ":" << strerror(errno) << endl;exit(-1);}return k;
}// 辅助函数,结合后面两个函数看
int getShmHelper(key_t k, int flags)
{int shmid = shmget(k, MAX_SIZE, flags);if (shmid < 0){cerr << errno << ":" << strerror(errno) << endl;exit(-1);}return shmid;
}// 获取shm,如果不存在就创建
int getShm(key_t k)
{return getShmHelper(k, IPC_CREAT);
}// 创建新的shm
int createShm(key_t k)
{return getShmHelper(k, IPC_CREAT | IPC_EXCL | 0600);
}// 挂接shm
void* attachShm(int shmid)
{void* mem = shmat(shmid, nullptr, 0);if (mem == (void*)(-1)){cerr << errno << ":" << strerror(errno) << endl;exit(-1);}return mem;
}// 去关联
void detachShm(void* mem)
{if (shmdt(mem) == -1){cerr << errno << ":" << strerror(errno) << endl;exit(-1);}
}// 删除shm
void delShm(int shmid)
{if(shmctl(shmid, IPC_RMID, nullptr) == -1){std::cerr << errno << " : " << strerror(errno) << std::endl;exit(-1);}
}

server.cpp中创建共享内存,然后等待接受信息:

// server.cpp#include "common.hpp"
#include <unistd.h> // 在该文件中会调用getpid()函数int main()
{key_t k = getKey();printf("key: 0x%x\n", k); // keyint shmid = createShm(k);printf("shmid: %d\n", shmid); // shmid// 直接以char*类型使用共享内存段,也就是把共享内存段存储的数据看成字符串// 后续使用就跟使用用malloc开辟的空间一样char *start = (char *)attachShm(shmid);printf("attach success, address start: %p\n", start);struct shmid_ds ds;           // 保存共享内存信息的结构体shmctl(shmid, IPC_STAT, &ds); // 获取共享内存的部分属性信息并打印一下printf("获取属性: size: %d, pid: %d, myself: %d, key: 0x%x\n",ds.shm_segsz, ds.shm_cpid, getpid(), ds.shm_perm.__key);// 使用while (true){printf("client say : %s\n", start);sleep(1);}// 去关联detachShm(start);sleep(10);delShm(shmid); // 删除共享内存return 0;
}

client.cpp中向共享内存写入信息:

// clent.cpp#include "common.hpp"
#include <unistd.h>		// 在该文件中会调用getpid()函数int main()
{key_t k = getKey(); printf("key: 0x%x\n", k);		// keyint shmid = getShm(k);printf("shmid: %d\n", shmid);	// shmidchar *start = (char*)attachShm(shmid);printf("attach success, address start: %p\n", start);const char* message = "hello server, 我是另一个进程,正在和你通信";pid_t id = getpid();int cnt = 1;while(true){snprintf(start, MAX_SIZE, "%s[pid:%d][消息编号:%d]", message, id, cnt++);sleep(1);}detachShm(start);return 0;
}

在client.cpp中一秒发送一条信息,在server.cpp中一秒打印一条信息,此时运行情况如下:
共享内存

共享内存的特点

在结束掉上面两个进程之后再次运行server
image-20240229141626208

此时会报错说文件已经存在了,也就是我们结束掉进程之后共享内存并没有自动销毁。

所以共享内存的生命周期是不随进程的,如果我们一直不释放,只有在关掉系统时消失,所以共享内存的生命周期是随OS的。

当然linux也有一些查看共享内存相关的命令,我们可以用ipcs -m命令查看共享内存:
image-20240229141924851

发现此时确实有一个共享内存,查看它的key和shmid信息也确实是我们刚刚创建的。其中几个属性还有owner,为创建共享内存的用户。perms就是permissions,权限的意思,正好也是我们创建共享内存时输入的权限0666。bytes就是共享内存的大小。nattach就是和共享内存挂接的进程的数量。

此时我们就可以用ipcrm -m $shmid指令手动释放掉共享内存:
image-20240229142330552

上面创建完共享内存之后的使用方式就跟使用malloc开辟的空间似的。因为在进程的视角来看那就是自己空间的一部分,直接使用,不像管道似的还要先把要发送的数据存起来开然后向管道中写入,多了向管道中写入和从管道中读出两次拷贝。所以共享内存是所有进程间通信的方式中最快的。

我们还发现了,在运行client之前server端已经向显示器打印信息了,即使共享内存中没有信息。在client端关掉之后,server端就一直打印client最后发送的信息。所以共享内存是不会像管道一样能进行同步和互斥,也不会对数据有任何的保护。同步和互斥是通过信号量来完成的。

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

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

相关文章

Nodejs基于vue的个性化服装衣服穿搭搭配系统sprinboot+django+php

本个性化服装搭配系统主要根据用户数据信息&#xff0c;推荐一些适合的搭配穿搭&#xff0c;同时&#xff0c;用户也可自己扫描上传自身衣物以及输入存放位置&#xff0c;搭配后存储到“我的搭配”中&#xff0c;以便下次挑选&#xff0c;既可以节省搭配时间&#xff0c;也方便…

Stable Video Diffusion(SVD)视频生成模型发布 1.1版

前言 近日&#xff0c;随着人工智能技术的飞速发展&#xff0c;图像到视频生成技术也迎来了新的突破。特别是Stable Video Diffusion&#xff08;SVD&#xff09;模型的最新版本1.1&#xff0c;它为我们带来了从静态图像生成动态视频的全新能力。本文将深入解析SVD 1.1版本的核…

Vue3使用JSX/TSX

文章目录 1. 什么是 JSX & TSX?JSX&#xff08;JavaScript XML&#xff09;TSX&#xff08;TypeScript XML&#xff09; 2.Vue3 中使用 TSX基本渲染 & 响应式 & 事件 3.JSX 和 template 哪个好呢&#xff1f;总结 1. 什么是 JSX & TSX? 提示&#xff1a;JSX…

【K8s】初识PV和PVC

​ 目录 收起 O、致谢 一、前言 二、Volume 2.1 什么是Volume 2.2 为什么要引入Volume 2.3 Volume类型有哪些 2.4 Volume如何使用 2.4.1 通过emptyDir共享数据 2.4.2 使用HostPath挂载宿主机文件 2.4.3 挂载NFS至容器 三、PV和PVC 3.1 什么是PV和PVC 3.2 为什么要引入PV和PVC 3…

【QT+QGIS跨平台编译】之五十九:【QGIS_CORE跨平台编译】—【错误处理:字符串错误】

文章目录 一、字符串错误二、处理方法三、涉及到的文件四、宽字节与多字节问题五、字符转换处理一、字符串错误 常量中有换行符错误: 也有const char * 到 LPCWSTR 转换的错误 二、处理方法 需要把对应的文档用记事本打开,另存为 “带有BOM的UTF-8” 三、涉及到的文件 src…

J17资本合伙人SKY LAI确认出席Hack .Summit() 2024区块链开发者盛会

J17资本合伙人SKY LAI确认出席由 Hack VC 主办&#xff0c;并由 AltLayer 和 Berachain 联合主办&#xff0c;与 SNZ 和数码港合作&#xff0c;由 Techub News 承办的Hack.Summit() 2024区块链开发者盛会。 J17资本合伙人SKY LAI负责管理公司的Web3基金&#xff0c;投资领域涵盖…

vivo 在离线混部探索与实践

作者&#xff1a;来自 vivo 互联网服务器团队 本文根据甘青、黄荣杰老师在“2023 vivo开发者大会"现场演讲内容整理而成。 伴随 vivo 互联网业务的高速发展&#xff0c;数据中心的规模不断扩大&#xff0c;成本问题日益突出。在离线混部技术可以在保证服务质量的同时&…

第104讲:数据库分库分表的意义与实现策略(MyCat)

文章目录 1.分库分表的目的2.分库分表的拆分策略2.1.垂直拆分2.2.水平拆分 3.Mycat水平拆分的分片规则 1.分库分表的目的 互联网中的应用程序&#xff0c;随着公司的发展&#xff0c;应用系统的使用人数、数据量都再持续增长&#xff0c;数据库层面就会产生一定的瓶颈。 如果…

Transformer之Residuals Decoder

The Residuals 我们需要提到的编码器架构中的一个细节是&#xff0c;每个编码器中的每个子层(self-attention,&#xff0c;ffnn)周围都有一个残余连接&#xff0c;然后是 layer-normalization 步骤。 如果我们要可视化向量和与 self attention 相关的 layer-norm 运算&#x…

基于视觉识别的自动采摘机器人设计与实现

一、前言 1.1 项目介绍 【1】项目功能介绍 随着科技的进步和农业现代化的发展&#xff0c;农业生产效率与质量的提升成为重要的研究对象。其中&#xff0c;果蔬采摘环节在很大程度上影响着整个产业链的效益。传统的手工采摘方式不仅劳动强度大、效率低下&#xff0c;而且在劳…

图像处理基础——频域、时域

傅里叶分析不仅仅是一个数学工具&#xff0c;更是一种可以彻底颠覆一个人以前世界观的思维模式。 一、什么是频域 时域 时域是信号在时间轴随时间变化的总体概括&#xff1b;频域是把时域波形的表达式做傅立叶等变化得到复频域的表达式&#xff0c;所画出的波形就是频谱图&a…

Docker技术概论(8):Docker Desktop原生图形化管理

Docker技术概论&#xff08;8&#xff09; Docker 原生图形化管理 - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this article:…

字节序转换函数

目录 为什么要字节序转换&#xff1f;网络协议指定通讯字节序为大端字节序转换函数主机字节序转换为网络字节序hton1 (host to network l是length指长整型)htons (host to network s是short指短整型)两个函数的代码案例 网络字节序转主机字节序ntoh1ntohs 为什么要字节序转换&a…

【报名指南】2024年第九届数维杯数学建模挑战赛报名全流程图解

1.官方报名链接&#xff1a; 2024年第九届数维杯大学生数学建模挑战赛http://www.nmmcm.org.cn/match_detail/32 2.报名流程&#xff08;电脑与手机报名操作流程一致&#xff09; 参赛对象为在校专科生、本科生、研究生&#xff0c;每组参赛人数为1-3人&#xff08;指导老师不…

【MySQL】数据查询——DQL基本数据库查询

目录 查询语法1. 查询表中所有的数据行和列&#xff0c;采用“*”符号2. 查询表中指定列的数据。3. 在查询中使用别名&#xff0c;使用“AS”关键字。4. 在查询中使用常量列&#xff1a;如果需要将一些常量的默认信息添加到输出结果中&#xff0c;以方便统计或计算。可以使用常…

Unity 预制体与变体

预制体作用&#xff1a; 更改预制体&#xff0c;则更改全部的以预制体复制出的模型。 生成预制体&#xff1a; 当你建立好了一个模型&#xff0c;从层级拖动到项目中即可生成预制体。 预制体复制模型&#xff1a; 将项目中的预制体拖动到层级中即可复制。或者选择物体复制粘贴。…

Jenkins自动化部署之流水线模式部署

文章目录 任务类型Pipeline流水线项目声明式的Pipeline脚本式Pipeline 示例脚本生成Tools配置示例 高级Pipeline Script from SCM 任务类型 在Jenkins中&#xff0c;有不同类型的任务&#xff08;项目&#xff09;适用于不同的构建需求。以下是一些常见的Jenkins任务类型&…

MYSQL03高级_新增用户、授予权限、授权底层表结构、角色理解

文章目录 ①. 登录服务器操作②. 用户的增删改③. 修改用户密码④. MySQL8密码管理⑤. 权限列表及原则⑥. 授予查看回收权限⑦. 底层权限表操作⑧. 角色的理解 ①. 登录服务器操作 ①. 启动MySQL服务后,可以通过mysql命令来登录MySQL服务器,命令如下: mysql –h hostname|hos…

32单片机基础:TIM输出比较

这个输出比较功能是非常重要的&#xff0c;它主要是用来输出PWM波形,PWM波形又是驱动电机的必要条件&#xff0c;所以你如果想用STM32做一些有电机的项目&#xff0c;比如智能车&#xff0c;机器人等。 IC: Input Capture 输入捕获 CC:Capture/Compare一般表示输入捕获和输出…

【学习心得】浏览器开发者工具中出现的VM开头的JS文件是什么?

一、现象描述 在Chrome的开发者工具中&#xff0c;你可能会看到一些以“VM”开头的JavaScript文件&#xff08;如“VM111.js”&#xff09;。 二、VM文件到底是什么&#xff1f; “VM”表示的是Virtual Machine&#xff08;虚拟机&#xff09;&#xff0c;这些文件通常表示由浏…