欢迎光临
我们一直在努力

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

本文所讲的共享内存为System V共享内存

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

文章目录

  • 1. 什么是共享内存
    • 1.2 共享内存的特点
    • 1.3 共享内存的工作原理
  • 2.在Linux中使用共享内存
    • 2.1 介绍System V
      • 2.1.1 创建共享内存
        • 2.1.1.1 `key`值的获取
      • 2.1.2 通过指令回收共享内存
      • 2.1.3 通过共享内存控制函数释放
      • 2.1.4 进程关联共享内存
      • 2.1.5 进程去关联
      • 2.1.6 再讲shmctl
    • 2.2 共享内存的简单使用
  • 3. 总结

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

    int shmget(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_t ftok(const char* 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>

    #define PATH "./"
    #define PROJID 0x333
    #define SIZE 4096
    const 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;
    }

    //创建共享内存
    int createShm(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;
    }

    //获取共享内存
    int getShm(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"

    int main()
    {
    //创建共享内存
    key_t key = getKey();
    int shmid = getShm(key,SIZE);
    std::cout<<"server key:"<<toHEX(key)<<std::endl;
    std::cout<<"server shmid:"<<shmid<<std::endl;
    return 0;
    }

    client.cc

    /**
    * 客户端,用于向服务端发送信息
    */

    #include "common.hpp"

    int main()
    {
    //创建共享内存
    key_t key = getKey();
    int shmid = createShm(key,SIZE);
    std::cout<<"cilent key:"<<toHEX(key)<<std::endl;
    std::cout<<"cilent shmid:"<<shmid<<std::endl;
    return 0;
    }

    下面是运行结果: 运行结果

    在shell命令行,我们可以通过指令ipcs -m来查看创建出来的共享内存 在这里插入图片描述

    该共享内存表的标识依次为:

    key值、shmid、拥有者、权限、大小、挂载数、状态

    2.1.2 通过指令回收共享内存

    无论我们使用那种通信方式,都要记得在使用结束后将资源释放。 对于共享内存的释放,我们可以在shell的命令行释放。使用指令ipcrm -m shmid 在这里插入图片描述

    当然,在命令行里释放还是太让人不舒适了,我们可以直接在程序中控制共享内存的释放。

    2.1.3 通过共享内存控制函数释放

    shmctl 是一个用于操作和管理 共享内存段 的 System V IPC 函数。它提供了对共享内存段的多种控制功能,比如删除共享内存段、获取信息、修改权限等。

    int shmctl(int shmid, int cmd, struct shmid_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"

    int main()
    {
    //创建共享内存
    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);
    return 0;
    }

    在这里插入图片描述

    可以看到5秒后,共享内存被释放了。

    2.1.4 进程关联共享内存

    共享内存的开辟就是为给不同的进程同一块空间的,那么我们要怎么给不同的进程看到同一块空间呢?就需要用到进程关联函数shmat.

    void *shmat(int shmid, const void *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"

    int main()
    {
    //创建共享内存
    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);
    return 0;
    }

    server.cc

    /**
    * 服务端,用于接受客户端发来的信息
    */

    #include "common.hpp"

    int main()
    {
    //创建共享内存
    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);
    return 0;
    }

    在这里插入图片描述

    从这个图中我们可以发现,关联数的变化。 最重要的是,我们的地址居然是不一样的,这也就更加印证来虚拟地址的存在,两个进程通过页表映射指向同一块空间。 程序结束后,会自动取消关联状态 当然我们也可以手动去关联。

    2.1.5 进程去关联

    进程去关联需要用到函数shmdt. shmdt 是用于分离共享内存段的函数,它将之前通过 shmat 附加到进程地址空间的共享内存段移除。调用此函数后,进程将无法通过原地址访问该共享内存段。

    int shmdt(const void *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"

    int main()
    {
    //创建共享内存
    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;
    struct shmid_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);
    return 0;
    }

    在这里插入图片描述

    共享内存 = 共享内存的内核数据结构(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>

    #define PATH "./"
    #define PROJID 0x333
    #define SIZE 4096

    class Shm{
    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;
    }
    int createShm()
    {
    /**
    * function:创建共享内存
    * return:返回共享内存的标识符
    * @return
    */

    int sid = shmget(key,SIZE,IPC_CREAT|IPC_EXCL|mode);
    assert(sid!=1);
    return shmid = sid;
    }
    int getShm()
    {
    /**
    * 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;
    }
    void deAssociation(void* start)
    {
    /**
    * function:进行去关联
    * :parame start 共享内存的起始地址
    */

    shmdt(start);
    }
    void release()
    {
    /**
    * function:释放共享内存
    */

    int n = shmctl(shmid,IPC_RMID,nullptr);
    assert(n!=1);
    }
    int getShmid()
    {
    /**
    * @return
    */

    return shmid;
    }
    };

    server.cc

    /**
    * 向cilent端发送信息
    */

    #include "common.hpp"

    int main()
    {
    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();
    return 0;
    }

    cilent.cc

    /**
    * 接受server的消息
    */

    #include "common.hpp"

    int main()
    {
    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();
    return 0;
    }

    运行结果: 运行结果

    3. 总结

    什么共享内存比管道快。 共享内存快的原因就在于比管道少了两次IO(输入输出)操作。IO是很慢的,怎么证明呢?

    #include <unistd.h>
    #include <signal.h>
    #include <cstdlib>
    #include <stdio.h>
    int cnt = 0;
    void alarm_handler(int signum) {
    printf("cnt: %d\\n", cnt);
    exit(1);
    }
    int main() {
    signal(SIGALRM, alarm_handler); // 注册信号处理器
    alarm(1); // 设置一个1秒的闹钟
    while(true)
    {
    printf("%d\\n",cnt++);
    }
    return 0;
    }

    在这里插入图片描述

    频繁的IO操作,导致cnt最终才累加到135073。 现在我们换个写法

    #include <unistd.h>
    #include <signal.h>
    #include <cstdlib>
    #include <stdio.h>
    int cnt = 0;
    void alarm_handler(int signum) {
    printf("cnt: %d\\n", cnt);
    exit(1);
    }
    int main() {
    signal(SIGALRM, alarm_handler); // 注册信号处理器
    alarm(1); // 设置一个1秒的闹钟
    while(true)
    {
    //printf("%d\\n",cnt++);
    cnt++;
    }
    return 0;
    }

    在这里插入图片描述

    效率差距非常之大。 管道: 管道

    共享内存: 共享内存

    共享内存的缺点

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

    感谢你的阅读,如果你有求职的需求可以看看这里

    在这里插入图片描述 往期Linux文章:Linux专栏

    赞(0)
    未经允许不得转载:171主机测评 » 【Linux】「共享内存揭秘」:高效进程通信的终极指南
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址