Linux的共享内存

Linux的共享内存

Louis 1131 2023-03-22

共享内存是System V版本的最后一个进程间通信方式。共享内存,顾名思义就是允许两个不相关的进程访问同一个逻辑内存,共享内存是两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常为同一段物理内存。进程可以将同一段物理内存连接到他们自己的地址空间中,所有的进程都可以访问共享内存中的地址。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。

特别提醒:共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取,所以我们通常需要用其他的机制来同步对共享内存的访问,例如信号量

共享内存的通信原理

在Linux中,每个进程都有属于自己的进程控制块(PCB)和地址空间(Addr Space),并且都有一个与之对应的页表,负责将进程的虚拟地址与物理地址进行映射,通过内存管理单元(MMU)进行管理。两个不同的虚拟地址通过页表映射到物理空间的同一区域,它们所指向的这块区域即共享内存。
共享内存的通信原理示意图:

image-1679414455184

当两个进程通过页表将虚拟地址映射到物理地址时,在物理地址中有一块共同的内存区,即共享内存,这块内存可以被两个进程同时看到。这样当一个进程进行写操作,另一个进程读操作就可以实现进程间通信。但是,我们要确保一个进程在写的时候不能被读,因此我们使用信号量来实现同步与互斥。
对于一个共享内存,实现采用的是引用计数的原理,当进程脱离共享存储区后,计数器减一,挂架成功时,计数器加一,只有当计数器变为零时,才能被删除。当进程终止时,它所附加的共享存储区都会自动脱离。

为什么共享内存速度最快?

借助上图说明:Proc A 进程给内存中写数据, Proc B 进程从内存中读取数据,在此期间一共发生了两次复制
(1)Proc A 到共享内存 (2)共享内存到 Proc B
因为直接在内存上操作,所以共享内存的速度也就提高了。

共享内存的指令

  1. 查看系统中的共享存储段
ipcs -m
  1. 删除系统中的共享存储段
ipcrm -m [shmid]

相关api

  1. shmget ( ):创建共享内存
int shmget(key_t key, size_t size, int shmflg);

key:由ftok生成的key标识,标识系统的唯一IPC资源。

size:需要申请共享内存的大小。在操作系统中,申请内存的最小单位为页,一页是4k字节,为了避免内存碎片,我们一般申请的内存大小为页的整数倍。

shmflg:如果要创建新的共享内存,需要使用IPC_CREAT,IPC_EXCL,如果是已经存在的,如果是打开一个已经存在的共享内存,size写0。

-IPC_CREAT | 创建共享内存
-IPC_CREAT | 0664 创建的时候给共享内存一个操作权限
-IPC_CREAT | IPC_EXCL :检测共享内存是否存在,存在则函数返回-1,不存在返回0

返回值:成功时返回一个新建或已经存在的的共享内存标识符,取决于shmflg的参数。失败返回-1并设置错误码。

  1. shmat ( ):

    挂接共享内存,启动对该共享内存的访问,并把共享内存连接到进程的地址空间

    void *shmat(int shmid, const void *shmaddr, int shmflg);
    

shmid:共享存储段的标识符。

*shmaddr:shmaddr = 0,则存储段连接到由内核选择的第一个可以地址上(推荐使用)。

shmflg:若指定了SHM_RDONLY位,则以只读方式连接此段,否则以读写方式连接此段。

返回值:成功返回共享存储段的指针(虚拟地址),并且内核将使其与该共享存储段相关的shmid_ds结构中的shm_nattch计数器加1(类似于引用计数);出错返回-1。

  1. shmdt ( )

当一个进程不需要共享内存的时候,就需要分离。该函数并不删除所指定的共享内存区,而是将之前用shmat函数连接好的共享内存区脱离目前的进程。

int shmdt(const void *shmaddr);

*shmaddr:连接以后返回的地址。

返回值:成功返回0,并将shmid_ds结构体中的shm_nattch计数器减1;出错返回-1。

  1. shmctl ( ):

用来控制共享内存的各种操作

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

shmid:共享存储段标识符。

cmd:指定的执行操作,设置为IPC_RMID时表示可以删除共享内存。

IPC_STAT:获取共享内存的状态
IPC_SET:如果进程有足够的权限,就可以设置共享内存状态
IPC_RMID:标记共享内存要被销毁,类似智能指针的引用计数,引用计数为零就把共享内存销毁,那传入的结构体可为空

*buf:设置为NULL即可。

返回值:成功返回0,失败返回-1。

该函数还可以获取共享内存的参数shmid_ds,或者对其进行设置。

其他项目中的使用情况

安卓中使用了类似共享内存的匿名共享内存,主要用在数据较大的传感器传输数据,比如相机和rgbd。

通常驱动端写入,然后通过回调发送信号给读取端,让读取端从对应的fd里面读取,以解决同步的问题

这种方式更类似于mmap,是基于文件描述符fd的传输方式

参考

进程间通信——共享内存(Shared Memory)YPT_victory的博客-CSDN博客共享内存

正文6:System V共享内存(修正版)及ftok函数讲解_谢白羽的博客-CSDN博客

接口封装

shdmem.h

//
// Created by louis on 2022/6/6.
//

#ifndef SHDMEM_TEST_SHDMEM_H
#define SHDMEM_TEST_SHDMEM_H

#include <memory>
#include <tuple>
#include "sys/shm.h"
#include "sys/ipc.h"
#include <sys/mman.h>
#include <fcntl.h>
#include<sys/ioctl.h>
#include <unistd.h>
#include <cstring>
#include "iostream"

namespace pudutech
{
    class Shdmem
    {
    public:
        ~Shdmem();
        static std::shared_ptr<Shdmem>  create();
        /**
         * 创建共享内存,设置共享内存的名字,指定创建的共享内存的大小
         * **/
        std::tuple<int32_t, char*> createMemory(int proj_id, int32_t size);

        /**
         * 释放创建的共享内存空间
         * **/
        bool destroyMemory();

        /**
         * 连接shared_memory_fd指向的共享内存
         * **/
        static std::shared_ptr<Shdmem> useMemory(int32_t shared_memory_key);


        /**
         * 获取连接的共享内存地址
         * **/
        char *getMemory();

    private:
        explicit Shdmem(bool create);
        bool linkSharedMemory(int32_t shared_memory_key);
        int getMemoryId(int32_t shared_memory_key_);
        bool releaseSharedMemory();
        bool closeSharedMemory();
        /* function */

    private:
        bool created_;
        int32_t shared_memory_key_;
        void *shared_memory_address_;
        /* data */
    };

} // namespace pudutech



#endif //SHDMEM_TEST_SHDMEM_H

shdmem.cpp 接口实现

//
// Created by louis on 2022/6/6.
//

#include "shdmem.h"


#define LOGW(TAG) std::cout << "wraning: " << TAG << " "
#define LOGV(TAG) std::cout << "info: " << TAG << " "

namespace pudutech {
    const char *ASHMEM_DEVIC = "/dev/ashmem";
    const char *SHTAG = "Shdmem";

    Shdmem::Shdmem(bool create) : created_(create), shared_memory_key_(-1), shared_memory_address_(nullptr) {}

    Shdmem::~Shdmem() {
        closeSharedMemory();
        if (created_) {
            releaseSharedMemory();
        }
    }

    std::shared_ptr<Shdmem> Shdmem::create() {
        return std::shared_ptr<Shdmem>(new Shdmem(true));
    }


    /**
     * 创建共享内存,设置共享内存的名字,指定创建的共享内存的大小
     * **/
    std::tuple<int32_t, char *> Shdmem::createMemory(const int proj_id, int32_t size) {
        //指定一个放置id管理的目录
        key_t key = proj_id;
        //如果用自定义的key,已经被其他ipc使用,会返回error
        if (key < 0) {
            LOGW(SHTAG) << "ftok error " << strerror(errno);
            return {-1, nullptr};
        }
        int shm_id = shmget(key, size, 0666 | IPC_CREAT);   //获得共享内存
        if (shm_id < 0) {
            LOGW(SHTAG) << "shmget error " << strerror(errno);
            return {-1, nullptr};
        }
        auto addr = shmat(shm_id, nullptr, 0660);
        if (!addr) {
            LOGW(SHTAG) << "shmat error " << strerror(errno);
            return {-1, nullptr};
        }
        shared_memory_key_ = key;
        shared_memory_address_ = addr;
        return {shared_memory_key_, (char *) shared_memory_address_};
    }

    /**
     * 释放创建的共享内存空间
     * **/
    bool Shdmem::destroyMemory() {
        if (created_) {
            if (closeSharedMemory()) {
                return releaseSharedMemory();
            } else return false;
        } else {
            //只做引用计数降低
            closeSharedMemory();
        }
        return false;
    }

    /**
     * 连接shared_memory_fd指向的共享内存
     * **/
    std::shared_ptr<Shdmem> Shdmem::useMemory(int32_t shared_memory_key) {
        auto Shdmem = std::shared_ptr<pudutech::Shdmem>(new pudutech::Shdmem(false));
        if (Shdmem->linkSharedMemory(shared_memory_key)) return Shdmem;
        return nullptr;
    }

    /**
     * 获取连接的共享内存地址
     * **/
    char *Shdmem::getMemory() {
        if (created_) return nullptr;
        return (char *) shared_memory_address_;
    }


    bool Shdmem::linkSharedMemory(int32_t shared_memory_key) {
        auto shmid = getMemoryId(shared_memory_key);
        if (shmid < 0) {
            LOGW(SHTAG) << "shmget error " << strerror(errno);
            return false;
        }
        auto addr = shmat(shmid, nullptr, 0660);
        if (!addr) {
            LOGW(SHTAG) << "shmat error " << strerror(errno);
            return false;
        } else {
            shared_memory_key_ = shared_memory_key;
            shared_memory_address_ = addr;
        }
        return true;
    }

    int Shdmem::getMemoryId(int32_t key){
         return shmget(key, 0, 0666);
    }

    /**
     * 释放内存,共享内存不再可以被链接
     * **/
    bool Shdmem::releaseSharedMemory() {
        if (shared_memory_key_ == -1 && shared_memory_address_ == nullptr) {
            LOGW(SHTAG) << "memory not  alloced";
            return true;
        }
        auto shmid = getMemoryId(shared_memory_key_);
        if (shmid < 0) {
            LOGW(SHTAG) << "shmget error " << strerror(errno);
            return false;
        }
        int ret = shmctl(shmid, IPC_RMID, nullptr);
        LOGV(SHTAG) << "shmctl return " << ret;
        if (ret != 0) {
            LOGW(SHTAG) << "remove memory failed, error " << errno << " " << strerror(errno);
            return false;
        }
        shared_memory_key_ = -1;
        return true;
    }

    /**
     * 关闭对内存的引用,不再可以访问内存
     * 内存不会被回收
     * **/
    bool Shdmem::closeSharedMemory() {
        if (shared_memory_key_ == -1 && shared_memory_address_ == nullptr) {
            LOGW(SHTAG) << "memory not  alloced";
            return true;
        }
        //减低引用计数
        auto ret = shmdt(shared_memory_address_);
        LOGV(SHTAG) << "close fd return " << ret;
        if (ret < 0) {
            LOGW(SHTAG) << "close file descriptor failed, error " << errno << " " << strerror(errno);
            return false;
        }
        shared_memory_address_ = nullptr;
        return true;
    }

} // namespace pudutech

接口使用:

写端:

auto shdmem = pudutech::Shdmem::create();
int key ;
char * addr;
std::tie(key, addr) = shdmem->createMemory(38, sizeof(data_in_out));

读端:

auto shdmem = pudutech::Shdmem::useMemory(38);
char * addr = shdmem->getMemory();

然后可以对取得的addr进行读写。

读端的key需要写端来传入,读端不会创建共享内存.