0


【Linux】「共享内存揭秘」:高效进程通信的终极指南

本文所讲的共享内存为

System V

共享内存

🏠 大家好,我是Yui_,一位努力学习C++/Linux的博主💬
🍑 如果文章知识点有错误的地方,请指正!和大家一起学习,一起进步👀
🚀 如有不懂,可以随时向我提问,我会全力讲解

🔥 如果感觉博主的文章还不错的话,希望大家关注、点赞、收藏三连支持一下博主哦~!
🔥 你们的支持是我创作的动力!
🧸 我相信现在的努力的艰辛,都是为以后的美好最好的见证!
🧸 人的心态决定姿态!
💬 欢迎讨论:如有疑问或见解,欢迎在评论区留言互动。
👍 点赞、收藏与分享:如觉得这篇文章对您有帮助,请点赞、收藏并分享!
🚀 分享给更多人:欢迎分享给更多对 Linux 感兴趣的朋友,一起学习!

文章目录

1. 什么是共享内存

共享内存(Shared Memory)是一种进程间通信(IPC,Inter-Process Communication)的方式,允许多个进程通过访问同一块内存区域来实现数据共享和快速通信。它是一种效率极高的通信机制,因为数据不需要在进程间进行复制,只需在同一块内存中直接读写即可。

1.2 共享内存的特点

  1. 高效:数据在高效内存区域是直接共享的,不需要在进程之间进行复制,从而减少了CPU和IO的消耗。
  2. 全局性:共享内存是所有附加到该内存的进程都可以访问的,由此它是一种全局资源。
  3. 同步机制依赖:虽然共享内存提供了数据共享的功能,但是不会自动提供对数据的访问同步机制。需要结合其他IPC方法(如信号量、互斥锁等)来避免多个进程同时读写时产生的数据竞争。

1.3 共享内存的工作原理

  1. 操作系统内核会在物理内存中分配一个共享内存段。
  2. 各个进程通过特定的标识符(shm_id)访问同一块共享内存空间。
  3. 共享内存区域是进程的地址空间外的内存,进程需要将其映射到自己的地址空间中才能访问。

还记得在进程地址空间时的内容吗?
共享内存的工作原理可以理解为:操作系统在内存中开辟了一块共享内存段,让两个不同的进程的虚拟地址同时对这块空间建立起映射关系,此时两个独立的进程能看到同一块空间,可以直接对此空间进行写入或者读取操作。
虚拟地址映射

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

函数
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

ftok

#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专栏


本文转载自: https://blog.csdn.net/2303_79015671/article/details/143998347
版权归原作者 Yui_ 所有, 如有侵权,请联系我们删除。

“【Linux】「共享内存揭秘」:高效进程通信的终极指南”的评论:

还没有评论