当前位置: 首页 > article >正文

共享内存的通信

目录

共享内存的挂接与取消挂接

sharememoty.hpp编写

server.cc / clent.cc /测试

IPC通信

使用命名管道保护


我们今天学习共享内存的通信!!!

共享内存的挂接与取消挂接

我们之前创建了共享内存,但是没有和进程想关联,没有关联进程这么调度,所以我们需要映射挂接到自己的目录,使用shmat()函数实现挂接:

三个参数,第一个就是shmget的返回值也就是ID,第二个参数是本地的虚拟地址,一般不需要自己设置,第三个参数一般设置为0,详情见上面。

取消挂接我们使用shmdt()函数,shmdt(Shared Memory Detach)是 System V 共享内存(Shared Memory)机制中的一个函数,用于将共享内存段从当前进程的地址空间分离(取消映射)。

一个参数就是shmat成功的返回值。

然后运行server并监视共享内存看结果:

咦,这么什么都没有,nattch这么还是0,不是应该有依赖了吗,重点不再nattch,这个perms这么也是0,在 ipcs 命令的输出中,perms 代表 IPC 资源的权限(Permissions),用于控制进程对共享内存、信号量或消息队列的访问权限。哦我们没有设置共享内存的权限导致的。

我们可以在创建共享内存的时候进行创建,shmflg可以设置权限如下:

设置为仅自己可读写,然后所有进程可见。

可以看到正常挂接并且解除了,我们回过头来看操作系统,挂接就需要虚拟地址申请空间,操作系统申请空间是按照块为单位申请的,要么4KB,2KB等等,假设我们的系统每次申请空间是4KB,4096个字节,那如果我们今天的共享内存设置成4098,那操作系统是不是需要申请8kb才可以装下,bytes下面应该是8kb。但是操作系统不会故意迎合人的思维和行为而创建,操作系统实际上是创建了8kb没错,但是仍然会显示4098做为实际占有,你在 ipcs -m 里看到的共享内存大小 不是你创建时指定的大小,而是操作系统按页对齐后的实际分配大小

那我们挂接完了就剩下通信了,但是目前这么写代码有点难看呀!,我们需要进行封装,为了使接口共享出来,我们将comm.hpp改为sharememory.hpp,作为共享空间,将一些公共的代码共享出来。如下:

sharememoty.hpp编写

具体框架如上,不用我过多解释了吧,清楚明了了。接着填写进去就可以了。值得注意的是,这个getshm也是获取shmid,但是里面的shmflg的选项不能含有IPC_EXCL,因为这个函数是获取已有的共享内存的shmid。

class sharememory
{
public:
    sharememory();
    ~sharememory();
    // 创建shm
    int creatshm()
    {
        umask(0);
        key_t k = ::ftok(path.c_str(), projid);
        if (k < 0)
        {
            cerr << "ftok fail!" << endl;
            return -1;
        }
        cout << "k: " << k << endl;
        // 创建共享内存
        int shmid = ::shmget(k, gshmsize, IPC_CREAT | IPC_EXCL | gmode);
        if (shmid < 0)
        {
            cerr << "shmget error" << endl;
            return -2;
        }
        cout << "shmid: " << shmid << endl;
        return shmid;
    }
    // 得到shm,已有的获取不创建
    int getshm()
    {
        key_t k = ::ftok(path.c_str(), projid);
        if (k < 0)
        {
            cerr << "ftok fail!" << endl;
            return -1;
        }
        cout << "k: " << k << endl;
        // 创建共享内存
        int shmid = ::shmget(k, gshmsize, IPC_CREAT | gmode);
        if (shmid < 0)
        {
            cerr << "shmget error" << endl;
            return -2;
        }
        cout << "shmid: " << shmid << endl;
        return shmid;
    }
    // 挂接本地地址
    void *attachshm(int shmid)
    {
        void* ret = shmat(shmid, nullptr, 0);
        if ((long long)ret == -1)
        {
            return nullptr;
        }
        cout << "attach done" << (long long)ret << endl;
        return ret;
    }
    // 解除映射
    void detachshm(void* ret)
    {
        ::shmdt(ret);
        cout << "detach done: " << (long long)ret << endl;
    }
    // 删除shm
    void deleteshm(int shmid)
    {
        shmctl(shmid, IPC_RMID, nullptr);
    }
};

我们看到创建shmid和得到的函数高度一致,所以我们可以将其设置为私有函数,然后再调用,放在冗余。然后shmflg部分完全可以由用户自己传递。注意参数传递的不一致!!!

class sharememory
{
private:
    int creatshmhelper(int shmflg)
    {
        umask(0);
        key_t k = ::ftok(path.c_str(), projid);
        if (k < 0)
        {
            cerr << "ftok fail!" << endl;
            return -1;
        }
        cout << "k: " << k << endl;
        // 创建共享内存
        int shmid = ::shmget(k, gshmsize, shmflg);
        if (shmid < 0)
        {
            cerr << "shmget error" << endl;
            return -2;
        }
        cout << "shmid: " << shmid << endl;
        return shmid;
    }
public:
    sharememory();
    ~sharememory();
    // 创建shm
    int creatshm()
    {
        return creatshmhelper(IPC_CREAT | IPC_EXCL | gmode);
    }
    // 得到shm,已有的获取不创建
    int getshm()
    {
        return creatshmhelper(IPC_CREAT | gmode);
    }
    // 挂接本地地址

接着测试是否挂接->取消挂接->删除shm的过程是否成功:

server.cc和clent中都遵循:创建/获取shmid,挂接,解除挂接,删除的规则,当然删除只需要创建的一方就可以了,我们这里选择server。在此之前被忘了对sharememory进行初始化对象。

server.cc / clent.cc /测试

接下来测试:

我们还可以进一步的进行改进,可以注意到一些如k等的值是可以放在类里面进行管理的,这样server和clent都可以直接看到并且都有一份。

我们仅仅放置 _shmid(共享内存id),key(k值),_addr(映射地址)作为私有。如下修改减少了return的必要。

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <sys/stat.h>
using namespace std;
const string path = "/home/yulin2/linuxtest";
int projid = 0x6666;
int gshmsize = 4098;
mode_t gmode = 0600;
class sharememory
{
private:
    int creatshmhelper(int shmflg)
    {
        //umask(0);
        _key = ::ftok(path.c_str(), projid);
        if (_key < 0)
        {
            cerr << "ftok fail!" << endl;
            return -1;
        }
        cout << "k: " << _key << endl;
        // 创建共享内存
        _shmid = ::shmget(_key, gshmsize, shmflg);
        if (_shmid < 0)
        {
            cerr << "shmget error" << endl;
            return -2;
        }
        cout << "shmid: " << _shmid << endl;
        return _shmid;
    }
public:
    sharememory()
       :_shmid(-1)
       ,_key(-1)
       ,_addr(nullptr)

    {}
    ~sharememory()
    {}
    // 创建shm
    void creatshm()
    {
        creatshmhelper(IPC_CREAT | IPC_EXCL | gmode);
    }
    // 得到shm,已有的获取不创建
    void getshm()
    {
        creatshmhelper(IPC_CREAT | gmode);
    }
    // 挂接本地地址
    void attachshm()
    {
        _addr = shmat(_shmid, nullptr, 0);
        if ((long long)_addr == -1)
        {
            cout << "attach error" << endl;
        }
        cout << "attach done" << (long long)_addr << endl;
    }
    // 解除映射
    void detachshm()
    {
        if (_addr != nullptr)
           ::shmdt( _addr);
        cout << "detach done: " << (long long) _addr << endl;
    }
    // 删除shm
    void deleteshm()
    {
        shmctl(_shmid, IPC_RMID, nullptr);
    }
    //打印属性
    void shmmeta()
    {}
private:
    int _shmid;
    key_t _key;
    void *_addr;
};

sharememory shm;

这个打印属性的函数在当前没有实现的必要。对应的.cc也要进行修改:

我就不监视了,毕竟肯定会看到nattch等于2的。

经过我们如上一堆骚操作,只是将框架搭好了,我们还没有开始通信呢,我们现在就开始:

IPC通信

我们直接选择在.cc中进行通信,我们首先要拿得到挂接的虚拟地址,所以我们添加函数getaddr,我们直接向这个地址内写入就可以了,因为这个地址相当于共享内存,因为我们挂接了的。字符串名字就是这个首地址,所以这个地址我们可以理解为字符串,当然我们也可以把它当成结构体等等。C++打印char* 相当于打印这个指针指向的字符串

 

我们直接在server里面打印这个字符串(那个指针),然后在clent里面写入这个空间,看结果。

#include"sharememory.hpp"
int main()
{
    shm.creatshm();
    shm.attachshm();
    //sleep(10);
    cout << "server attach done" << endl;
    //进行IPC
    char* strinfo = (char*)shm.getaddr();
    while(true)
    {
        printf("%s\n", strinfo);
        sleep(1);
    }
    shm.detachshm();
    cout << "server detach done" << endl;
    //sleep(10);
    shm.deleteshm();
    cout << "detel shm" << endl;
    return 0;
}
#include"sharememory.hpp"
int main()
{
    shm.getshm();
    shm.attachshm();
    //sleep(10);
    cout << "clent attach done" << endl;
    char* strinfo = (char*)shm.getaddr();
    char ch = 'A';
    while(ch <= 'Z')
    {
        sleep(3);
        strinfo[ch - 'A'] = ch;
        ch++;
    }

    shm.detachshm();
    cout << "clent detach done" << endl;
    return 0;
}

我们可以看到当我们只打开读端时,server并没有像管道那样没有东西可读就停止,而是直接读空,当clent以三秒一次写入时,读端确实读了,但是没有去重呀,可见共享内存的读取是不等写进程了自我独立运行的,是让两个进程各自共享用户空间内存块,当我们关闭读端时,写端还在写入,反之也是类似的,所以没有加任何保护机制,这个很危险的,这种让进程具有独立性的方法,容易让两进程数据不一致(一端还在写,一端已经读取了),我们就需要对共享内存自己完成保护,我们一般使用加锁,或者使用数据量保护处于临界区的临界资源。基于管道的性质,我们还可以使用命名管道进行保护!!!但是这种方法也是有风险的,所以加锁最好!!!

共享内存不需要系统级的文件操作而是直接写入,所以这就意味着写入共享内存不存在文件缓冲区,所以通信速度最快。进程是直接看到映射的共享内存,所以不需要文件操作。

使用命名管道保护

写端在循环写入共享内存的同时先写入命名管道,等单次写入命名管道完成时,再提醒读端读取同时读取共享内存的内容,所以读端先访问管道,这是单向的,如果要完成双向的通信就需要两个命名管道。

命名管道在此的作用就是提醒读取的作用,等单次写入完成了再读,没有数据就不要读取,但是这样还是没有避免重复读取,只是避免了读取写入不一致,本质还是要到共享内存读的。命名管道(FIFO)在这里的作用更像是一个通知机制,用于同步写入和读取的时机,而真正的数据还是存放在共享内存(Shared Memory, SHM)中。解决共享内存的数据一致性问题,还是必须考虑加锁。如果多个进程可能同时访问共享内存,就需要同步机制来确保数据的正确性。常见的同步手段包括互斥锁(mutex)、读写锁、信号量等。


http://www.kler.cn/a/585515.html

相关文章:

  • 境内深度合成服务算法备案通过名单分析报告
  • 西游记英文版108天社里学练活动总结与感言
  • .NET Core 中如何实现缓存的预热?
  • 卷积神经网络 - 基本概念
  • 用Maven创建只有POM文件的项目
  • clickhouse网络安全日志 网络安全日志保存时间
  • Linux下学【MySQL】中如何实现:多表查询(配sql+实操图+案例巩固 通俗易懂版~)
  • pytorch中的基础数据集
  • 深搜专题8:N皇后
  • 最节省服务器,手搓电子证书查询系统
  • Navicat安装流程
  • 鸿蒙 @ohos.arkui.dragController (DragController)
  • 深度学习中学习率调整策略
  • NetLink内核套接字案例分析
  • 程序化广告行业(13/89):DSP的深入解析与运营要点
  • CH340 模块的作用及其与 JTAG、串口下载和 TTL 电平的关系
  • 【春招笔试】2025.03.13-蚂蚁春招笔试题
  • VisionPro中IPO工具详解
  • 代码随想录二刷|图论7
  • 【品铂科技工业生产应用案例解析】