本文所讲的共享内存为
System V
共享内存
🏠 大家好,我是Yui_,一位努力学习C++/Linux的博主
💬
🍑 如果文章知识点有错误的地方,请指正!和大家一起学习,一起进步👀
🚀 如有不懂,可以随时向我提问,我会全力讲解
🔥 如果感觉博主的文章还不错的话,希望大家关注、点赞、收藏三连支持一下博主哦~!
🔥 你们的支持是我创作的动力!
🧸 我相信现在的努力的艰辛,都是为以后的美好最好的见证!
🧸 人的心态决定姿态!
💬 欢迎讨论:如有疑问或见解,欢迎在评论区留言互动。
👍 点赞、收藏与分享:如觉得这篇文章对您有帮助,请点赞、收藏并分享!
🚀 分享给更多人:欢迎分享给更多对 Linux 感兴趣的朋友,一起学习!
文章目录
1. 什么是共享内存
共享内存(Shared Memory)是一种进程间通信(IPC,Inter-Process Communication)的方式,允许多个进程通过访问同一块内存区域来实现数据共享和快速通信。它是一种效率极高的通信机制,因为数据不需要在进程间进行复制,只需在同一块内存中直接读写即可。
1.2 共享内存的特点
- 高效:数据在高效内存区域是直接共享的,不需要在进程之间进行复制,从而减少了CPU和IO的消耗。
- 全局性:共享内存是所有附加到该内存的进程都可以访问的,由此它是一种全局资源。
- 同步机制依赖:虽然共享内存提供了数据共享的功能,但是不会自动提供对数据的访问同步机制。需要结合其他IPC方法(如信号量、互斥锁等)来避免多个进程同时读写时产生的数据竞争。
1.3 共享内存的工作原理
- 操作系统内核会在物理内存中分配一个共享内存段。
- 各个进程通过特定的标识符(shm_id)访问同一块共享内存空间。
- 共享内存区域是进程的地址空间外的内存,进程需要将其映射到自己的地址空间中才能访问。
还记得在进程地址空间时的内容吗?
共享内存的工作原理可以理解为:操作系统在内存中开辟了一块共享内存段,让两个不同的进程的虚拟地址同时对这块空间建立起映射关系,此时两个独立的进程能看到同一块空间,可以直接对此空间进行写入或者读取操作。
2.在Linux中使用共享内存
虽然Linux提供了
POSIX
和
System V
两种共享内存接口。但是本文将聚焦于
System V
2.1 介绍System V
System V(读作“System Five”)是 UNIX 操作系统家族的一个版本,由美国 AT&T 的贝尔实验室开发。它是早期 UNIX 的一个重要分支,并对后来的 UNIX 系统以及其他现代操作系统产生了深远影响。
虽然 System V 本身已经很少被直接使用,但它的思想和功能在现代操作系统中得到了传承。
比如:POSIX:吸收了 System V 的许多特性,成为跨平台的通用标准。
2.1.1 创建共享内存
创建共享内存时,需要使用到
shmget
函数
intshmget(key_t key,size_t size,int shmflg)
返回值:创建成功返回共享内存的
shmid
,失败返回
-1
。
参数介绍:
- key:共享内存的标识符。
- size:共享内存的大小,一般为
4096
的整数倍。 - shmflg:设置共享内存的创建方式以及创建权限。关于返回值 在OS中,共享内存也拥有自己的数据结构,所以返回值有点类似于文件系统中的
fd
,用于对不同的共享内存块进行操作。关于参数2 可能有读者会感到疑惑,为什么共享内存的大小是4096
字节的整数倍,这是因为大小刚好与PAGE
页大小相同,有利与提高IO
效率。但是你可以设置不是4096
大小的内存,不过OS在底层依然会分配向上取整的4096
整数倍的空间给共享内存,可是你只能使用你设置的空间大小。关于参数3 该参数为位图操作,也是就是状态压缩。与open
函数的参数2类似,常用的选项有: IPC_CREAT
创建共享内存,如果存在则使用已经存在的共享内存。IPC_EXCL
避免使用已经存在的共享内存,单独使用没有意义,需要配合IPC_CREAT
使用,当使用已经创建的共享内存时,会创建失败。权限
共享内存也是文件,需要权限掩码如0666
。关于参数1 key是共享内存的标识符,是让不同进程看到同一块空间的关键,虽然可以自己指定一个数字,但是OS提供了一个根据目标路径+项目编号生成独一无二数字的特殊算法有点类似于哈希。key_t
类型也就是对int
类型的封装,来表示一个数字,用来标识不同的共享内存块,可以理解为inode
。
2.1.1.1
key
值的获取
OS提供了函数
ftok
来生成
key
#include<sys/types.h>#include<sys/ipc.h>key_tftok(constchar* pathname,int proj_id);
返回值:
- 返回生成的标识符。 参数:
pathname
:项目路径,绝对/相对都可以。proj_id
:项目编号,自定义。key是打开共享内存的钥匙,让两个进程看到同一块空间的关键就在于key 下面我们写一段代码来看看吧,依然是吧代码分为3个部分。 公共部分common.hpp
,服务部分server.cc
,客户部分client.cc
common.hpp
/**
* 公共文件,用于key值获取,进制转化,创建共享内存,获取共享内存
*/#include<iostream>#include<sys/types.h>#include<sys/ipc.h>#include<sys/shm.h>#include<cassert>#include<unistd.h>#include<string>#include<cstring>#definePATH"./"#definePROJID0x333#defineSIZE4096const mode_t mode =0666;//机制转化,转16机制
std::string toHEX(int x){/**
* parame: x:一个十进制整数
* return: string:16进制字符串
* @return
*/char buff[128];snprintf(buff,sizeof(buff),"0x%x",x);return buff;}//获取key
key_t getKey(){/**
* return : ket_t :返回key值
* @return
*/
key_t key =ftok(PATH,PROJID);assert(key!=-1);//失败就终止进程 return key;}//创建共享内存intcreateShm(key_t key,size_t size){/**
* parame: key 标识符
* param: size 创建size大小的共享内存
* return: 返回共享内存id
* @return
*/int id =shmget(key,size,IPC_CREAT|IPC_EXCL|mode);assert(id!=-1);return id;}//获取共享内存intgetShm(key_t key,size_t size){/**
* parame: key 标识符
* param: size 创建size大小的共享内存
* return: 返回共享内存id
* @return
*/int id =shmget(key,size,IPC_CREAT);assert(id!=-1);return id;}
server.cc
/**
* 服务端,用于接受客户端发来的信息
*/#include"common.hpp"intmain(){//创建共享内存
key_t key =getKey();int shmid =getShm(key,SIZE);
std::cout<<"server key:"<<toHEX(key)<<std::endl;
std::cout<<"server shmid:"<<shmid<<std::endl;return0;}
client.cc
/**
* 客户端,用于向服务端发送信息
*/#include"common.hpp"intmain(){//创建共享内存
key_t key =getKey();int shmid =createShm(key,SIZE);
std::cout<<"cilent key:"<<toHEX(key)<<std::endl;
std::cout<<"cilent shmid:"<<shmid<<std::endl;return0;}
下面是运行结果:
在shell命令行,我们可以通过指令
ipcs -m
来查看创建出来的共享内存
该共享内存表的标识依次为:
key值、shmid、拥有者、权限、大小、挂载数、状态
2.1.2 通过指令回收共享内存
无论我们使用那种通信方式,都要记得在使用结束后将资源释放。
对于共享内存的释放,我们可以在shell的命令行释放。使用指令
ipcrm -m shmid
当然,在命令行里释放还是太让人不舒适了,我们可以直接在程序中控制共享内存的释放。
2.1.3 通过共享内存控制函数释放
shmctl
是一个用于操作和管理 共享内存段 的 System V IPC 函数。它提供了对共享内存段的多种控制功能,比如删除共享内存段、获取信息、修改权限等。
intshmctl(int shmid,int cmd,structshmid_ds*buf);
参数:
shmid
:共享内存标识符。cmd
:控制操作的命令,有以下值: -IPC_STAT
-IPC_SET
-IPC_RMID
:标识共享内存段为删除状态,等待所有关联进程分离后释放。buf
:指向struct shmid_ds
的指针,用于传递或者接收共享内存段的元段数据信息。 - 在IPC_RMID
模式下:buf
可设置为nullptr
. 返回: 成功返回0,失败返回-1。 修改以下代码
client.cc
/**
* 客户端,用于向服务端发送信息
*/#include"common.hpp"intmain(){//创建共享内存
key_t key =getKey();int shmid =createShm(key,SIZE);
std::cout<<"cilent key:"<<toHEX(key)<<std::endl;
std::cout<<"cilent shmid:"<<shmid<<std::endl;//释放共享内存int cnt =5;while(cnt){
std::cout<<cnt<<std::endl;sleep(1);
cnt--;}shmctl(shmid,IPC_RMID,nullptr);return0;}
可以看到5秒后,共享内存被释放了。
2.1.4 进程关联共享内存
共享内存的开辟就是为给不同的进程同一块空间的,那么我们要怎么给不同的进程看到同一块空间呢?就需要用到进程关联函数
shmat
.
void*shmat(int shmid,constvoid*shmaddr,int shmflg);
参数:
shmid
:共享内存的标识符。shmaddr
:用于指定共享内存要附加的地址,如果提供nullptr
,操作系统会自动选择一个合适的地址。shmflg
:附加时的默认标志: -0
:默认行为,读写属性。-SHM_RDONLY
:只读模式 返回值: 成功返回共享内存的首地址,失败返回(void*)-1关于参数2 默认情况我们就传nullptr,让OS自己去找。关于参数3 默认情况也直接传0,默认为读写属性。关于返回值 一般情况下,我们的通信都是通过字符来进行通信的,所有我们可以将其强转为char*
. 然后我们再来改一改,client.cc和server.cc
的代码
client.cc
/**
* 客户端,用于向服务端发送信息
*/#include"common.hpp"intmain(){//创建共享内存
key_t key =getKey();int shmid =createShm(key,SIZE);
std::cout<<"cilent key:"<<toHEX(key)<<std::endl;
std::cout<<"cilent shmid:"<<shmid<<std::endl;//开始进行进程关联char* start =(char*)shmat(shmid,nullptr,0);if((void*)start ==(void*)-1){perror("shmat");shmctl(shmid,IPC_RMID,nullptr);exit(1);}//成功printf("client start:%p\n",start);//打印地址看看//释放共享内存sleep(5);shmctl(shmid,IPC_RMID,nullptr);return0;}
server.cc
/**
* 服务端,用于接受客户端发来的信息
*/#include"common.hpp"intmain(){//创建共享内存
key_t key =getKey();int shmid =getShm(key,SIZE);
std::cout<<"server key:"<<toHEX(key)<<std::endl;
std::cout<<"server shmid:"<<shmid<<std::endl;//开始关联char* start =(char*)shmat(shmid,nullptr,0);if((void*)start==(void*)-1){perror("shmat");exit(1);}//成功printf("server start:%p\n",start);sleep(3);return0;}
从这个图中我们可以发现,关联数的变化。
最重要的是,我们的地址居然是不一样的,这也就更加印证来虚拟地址的存在,两个进程通过页表映射指向同一块空间。
程序结束后,会自动取消关联状态
当然我们也可以手动去关联。
2.1.5 进程去关联
进程去关联需要用到函数
shmdt
.
shmdt
是用于分离共享内存段的函数,它将之前通过
shmat
附加到进程地址空间的共享内存段移除。调用此函数后,进程将无法通过原地址访问该共享内存段。
intshmdt(constvoid*shmaddr);
参数:
shmaddr
:共享内存段的首地址,必须是之前通过shmat
返回的地址。 返回值 成功返回1,失败返回-1 注意:共享内存被删除后,已成功挂接的进程仍然可以继续进行正常的通信,不过此时无法再关联其他进程。
2.1.6 再讲shmctl
在上文,笔者已经使用了它的释放共享内存的功能了。除此之外,它还具有其他的功能。
当我们给参数2传递以下的参数时:
IPC_STAT
:获取或设置控制共享内存的数据结构。IPC_SET
:在进程有足够权限的情况下,将共享内存的当前关联值设置为buf
数据结构中的值。buf
就是共享内存的数据结构,可以使用IPC_STAT
获取,也可以使用IPC_SET
设置。 除去释放共享内存的IPC_RMID
不需要传递参数3,这两种情况都是需要传递参数3的。
通过
shmctl
获取共享内存的数据结构,并获取
pid
和
key
.
/**
* 客户端,用于向服务端发送信息
*/#include"common.hpp"intmain(){//创建共享内存
key_t key =getKey();int shmid =createShm(key,SIZE);
std::cout<<"cilent key:"<<toHEX(key)<<std::endl;
std::cout<<"cilent shmid:"<<shmid<<std::endl;//开始进行进程关联char* start =(char*)shmat(shmid,nullptr,0);if((void*)start ==(void*)-1){perror("shmat");shmctl(shmid,IPC_RMID,nullptr);exit(1);}//成功printf("client start:%p\n",start);//打印地址看看
std::cout<<"============="<<std::endl;structshmid_ds buf;int n =shmctl(shmid,IPC_STAT,&buf);if(n ==-1){perror("shmctl");shmctl(shmid,IPC_RMID,nullptr);exit(1);}
std::cout<<"buf.shm_cpid:"<<buf.shm_cpid<<std::endl;
std::cout<<"buf.shm_perm.__key:"<<toHEX(buf.shm_perm.__key)<<std::endl;//释放共享内存shmdt(start);//去关联shmctl(shmid,IPC_RMID,nullptr);return0;}
共享内存 = 共享内存的内核数据结构(struct shmid_ds)+真正开辟的空间
2.2 共享内存的简单使用
创建、关联共享内存
服务端向客户端写入数据
客户端端每个一秒读取一次。
common.cc
#include<iostream>#include<sys/types.h>#include<sys/ipc.h>#include<sys/shm.h>#include<cassert>#include<unistd.h>#include<string>#include<cstring>#definePATH"./"#definePROJID0x333#defineSIZE4096classShm{private:
key_t key;int shmid;
mode_t mode =0666;public:Shm(){
key =getKey();}
key_t getKey(){/**
* function:返回并设置key值
* return: 返回key值
* @return
*/
key_t k =ftok(PATH,PROJID);assert(k!=-1);return key = k;}intcreateShm(){/**
* function:创建共享内存
* return:返回共享内存的标识符
* @return
*/int sid =shmget(key,SIZE,IPC_CREAT|IPC_EXCL|mode);assert(sid!=-1);return shmid = sid;}intgetShm(){/**
* function:获取共享内存
* return: 返回共享内存的标识符
* @return
*/int sid =shmget(key,SIZE,IPC_CREAT);assert(sid!=-1);return shmid = sid;}char*processAssociation(){/**
* function:进行进程于共享内存间的关联
* return:返回共享内存的是起始地址
* @return
*/char* start =(char*)shmat(shmid,nullptr,0);assert((void*)start!=(void*)-1);return start;}voiddeAssociation(void* start){/**
* function:进行去关联
* :parame start 共享内存的起始地址
*/shmdt(start);}voidrelease(){/**
* function:释放共享内存
*/int n =shmctl(shmid,IPC_RMID,nullptr);assert(n!=-1);}intgetShmid(){/**
* @return
*/return shmid;}};
server.cc
/**
* 向cilent端发送信息
*/#include"common.hpp"intmain(){
Shm shm;
shm.createShm();//创建char* start = shm.processAssociation();//关联printf("server:");for(int i =0;i<26;++i){
start[i]=('a'+i);printf("%c",start[i]);fflush(stdout);
start[i+1]=0;sleep(1);}
shm.deAssociation(start);
shm.release();return0;}
cilent.cc
/**
* 接受server的消息
*/#include"common.hpp"intmain(){
Shm shm;
shm.getShm();char* start = shm.processAssociation();for(int i =0;i<26;++i){printf("cilent :%s\n",start);sleep(1);}
shm.deAssociation(start);
shm.release();return0;}
运行结果:
3. 总结
什么共享内存比管道快。
共享内存快的原因就在于比管道少了两次
IO
(输入输出)操作。IO是很慢的,怎么证明呢?
#include<unistd.h>#include<signal.h>#include<cstdlib>#include<stdio.h>int cnt =0;voidalarm_handler(int signum){printf("cnt: %d\n", cnt);exit(1);}intmain(){signal(SIGALRM, alarm_handler);// 注册信号处理器alarm(1);// 设置一个1秒的闹钟while(true){printf("%d\n",cnt++);}return0;}
频繁的IO操作,导致cnt最终才累加到135073。
现在我们换个写法
#include<unistd.h>#include<signal.h>#include<cstdlib>#include<stdio.h>int cnt =0;voidalarm_handler(int signum){printf("cnt: %d\n", cnt);exit(1);}intmain(){signal(SIGALRM, alarm_handler);// 注册信号处理器alarm(1);// 设置一个1秒的闹钟while(true){//printf("%d\n",cnt++);
cnt++;}return0;}
效率差距非常之大。
管道:
共享内存:
共享内存的缺点
多个进程无限制地访问同一块区域,导致共享内存的数据无法确保安全。
共享内存没有同步和互斥机制,某个进程可能数据还没有写完,就被别人读走了,或者被覆盖了。
感谢你的阅读,如果你有求职的需求可以看看这里
往期Linux文章:Linux专栏
版权归原作者 Yui_ 所有, 如有侵权,请联系我们删除。