【Linux】System V 共享内存

一、共享内存

1.1 共享内存的原理:

两个进程,操作系统在内存空间中创建一个共享内存。在之前学习库的时候,有一个共享库的概念。我们可以按照其概念来了解共享内存的概念:将共享内存映射到页表中,和进程的地址空间建立联系。我们可以将共享内存的虚拟地址交给用户。两个进程之间就可以通过虚拟地址找到同一个共享内存进行通信。

1.2 理解:

  • 上面说的操作,只能是操作系统来做
  • 操作系统必须提供上面步骤的系统调用,供系统A、B进行调用
  • 共享内存在系统中有多份,供不同个数,不同对的操作系统同时进行通信
  • 操作系统要对共享内存进行管理,先描述再组织
  • 共享内存并不是简单的一段空间,也要有描述并管理共享内存的数据结构和匹配的算法
  • 共享内存 = 共享空间(数据) + 共享内存的属性

二、共享内存的数据结构

cpp 复制代码
struct shmid_ds {
    struct ipc_perm shm_perm; /* operation perms */
    int shm_segsz; /* size of segment (bytes) */
    __kernel_time_t shm_atime; /* last attach time */
    __kernel_time_t shm_dtime; /* last detach time */
    __kernel_time_t shm_ctime; /* last change time */
    __kernel_ipc_pid_t shm_cpid; /* pid of creator */
    __kernel_ipc_pid_t shm_lpid; /* pid of last operator */
    unsigned short shm_nattch; /* no. of current attaches */
    unsigned short shm_unused; /* compatibility */
    void *shm_unused2; /* ditto - used by DIPC */
    void *shm_unused3; /* unused */
};

三、共享内存的使用

3.1 一些有关函数

3.1.1 shmget函数

函数的原型:

函数的参数:

  • key:这个共享内存端的名字
  • size:共享内存的大小
  • shmfig:由九个权限表标志构成,用法和创建文件时使用的mode模式标志是一样的,利用位图进行传参。

在这里的标识码有两个:IPC_CREAT和IPC_EXCL。

  • IPC_CREAT:如果内核中不存在键值和key相同的共享内存,则新建一个共享内存;如果存在这样的共享内存,则返回该共享内存的标识码(可以获得一个存在的共享内存)
  • IPC_EXCL:单独使用没有意义,只有和IPC_CREAT一起使用才有意义
  • IPC_CREAT | IPC_EXCL:如果内核中不存在键值和key相同的共享内存,则新建一个共享内存;如果存在这样的共享内存,则报错(如果我成功返回,这个shm则是一个新的共享内存)

函数的返回值:

成功则返回一个非负整数,即给共享内存段的标识码;失败则返回-1

3.1.2 ftok函数

ftok函数的原型:

函数的功能:

系统建立IPC通讯(如消息队列、共享内存等) 必须指定一个ID值。通常情况下,该id值通过ftok函数得到。
函数的参数:

  • pathname:必须是一个已经存在且程序可范围的文件。
  • proj_id:虽然定义为一个整数,其实实际只有8个bit位有效,即如果该参数大于255,则只有后8bit有效。

函数的返回值:

当函数执行成功,则会返回key_t键值,否则返回-1。在一般的UNIX中,通常是将文件的索引节点取出,然后在前面加上子序号就得到key_t的值。

3.1.3 stmctl函数

shmctl函数的原型:

函数的功能:

用来控制共享内存

函数的参数:

  • shmid:有shmget函数返回的共享内存的标识码
  • cmd:将要采取的动作(有三个可取值)

|----------|---------------------------------------------|
| 命令 | 说明 |
| IPC_STAT | 把shmid_ds结构中的数据设置为共享内存的当前关联值 |
| IPC_SET | 在进程有足够权限的前提下,把共享内粗的当前关联值设为shmid_ds数据结构中给出的值 |
| IPC_RMID | 删除共享内存段 |

  • buf:指向一个保存着恭喜昂内存的模式状态和访问权限的数据结构

函数的返回值:

成功返回0,失败返回-1

3.1.4 shmat函数

函数的原型:

函数的功能:

将创建好的共享内存连接到某个进程,并指定内存空间
函数的参数:

  • shmid:shmget返回的共享内存标识
  • shmaddr:把共享内存连接到当前进程去的时候准备放置它的那个地址
cpp 复制代码
shmaddr:把共享内存连接到当前进程去的时候准备放置它的那个地址,如果设为NULL为让系统自动选择。

    shmaddr为NULL,核心自动选择一个地址

    shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。

    shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:           shmaddr - (shmaddr % SHMLBA)
  • shmflg:其是一组按位OR(或)在一起的标志。它的两个可能取值是SHM_RND和SHM_RDONLY,shmflg=SHM_RDONLY,表示连接操作用来只读共享内存

函数的返回值:

如果成功返回一个指针,指向共享内存的第一个字节;如果失败,则返回-1

3.1.5 shmdt函数

函数的原型:

函数的功能:

将共享内存段与当前进程脱离
函数的参数

  • shmaddr:由shmat返回的指针

函数的返回值:

如果成功返回0;失败则返回-1

3.2 获取key值

只要拥有相同的文件路径和一个数字,我们可以使用这个函数来使服务端和客户端形成的key是一样的,代码如下:

cpp 复制代码
key_t GetCommonKey(const std::string& pathname, const int proj_id)
{
    key_t k = ftok(pathname.c_str(), proj_id);

    if(k < 0)
    {
        perror("GetCommonKey");
    }
    return k;
}

3.3 有关共享内存的一些指令

共享内存不会随着进程的结束而自动释放,如果不进行释放,该共享内存会一直存在,需要手动进行释放, 共享内存的生命周期随内核 | 文件的生命周期是随进程的。

cpp 复制代码
ipcs -m; // 进行共享内存的查看
ipcrm -m key // 进行共享内存的删除
  • 我们可以使用 ipcs -m指令查看共享内存
  • 我们可以使用 ipcrm -m key指令删除共享内存

3.4 key VS shmid

key属于用户形成,内核使用的字段,用户不能使用key来进行shm的管理,内核进行区分shm的唯一性的

shmid属于内核给用户返回的一个标识符,用来进行用户及对共享内存进行管理的id值。

3.5下面来进行对shm的准备工作

3.5.1 shm的创建和销毁工作

3.5.1.1 使进程之间获取一个相同的key值
cpp 复制代码
// 获取一个相同的key值,需要访问一个相同的文件路径和一个随便的数字
const std::string pathname = "/home/hrx/code/111/lesson3/shm";
const int proj_id = 0x66;

key_t GetKeyCommon()
{
    key_t k = ftok(_pathname.c_str(), _proj_id);

    if (k < 0)
    {
        perror("GetCommonKey");
    }
    return k;
}
3.5.1.2 通过key值打开共享内存块
cpp 复制代码
    int Shmget(int key, int size, int flag)  // 将key值的共享内存创建出来
    {
        int shmid = shmget(key, size, flag);
        if (shmid < 0)
        {
            perror("shmid");
        }
        return shmid;
    }

    std::string ToHex(int u)  // 将一个数字转换为十六进制的格式
    {
        char buff[128];
        snprintf(buff, sizeof buff, "0x%x", u);
        return buff;
    }

    Shm(const std::string &pathname, int proj_id, int who)  // 构造函数
        : _pathname(pathname), _proj_id(proj_id), _who(who)
    {
        _key = GetKeyCommon();
        if (who == Creater)
            GetShmCeaterUse();
        else
            GetShmCeaterUse();
    }

    bool GetShmCeaterUse()
    {
        if (_who == Creater)
        {
            _shmid = Shmget(_key, 1024, IPC_CREAT | IPC_EXCL);
            sleep(10);
            if (_shmid > 0)
                return true;
        }
        return false;
    }

    bool GetShmForUse()
    {
        if (_who == User)
        {
            _shmid = Shmget(_key, 1024, IPC_CREAT);
            if (_shmid > 0)
                return true;
        }
        return false;
    }
3.5.1.3 销毁共享内存
cpp 复制代码
    ~Shm()
    {
        if (_who == Creater)
        {
            // 进行共享库的删除
            int res = shmctl(_shmid, IPC_RMID, nullptr);
        }
        std::cout << "Shm remove done..." << std::endl;
    }

3.5.2 进行shm的挂接和取消挂接

3.5.2.1 进行shm的挂接
cpp 复制代码
    void* Attach()
    {
        void* attaddr = shmat(_shmid, nullptr, 0);
        if(attaddr == nullptr)
        {
            perror("attaddr");
        }
        std::cout << "name: " << GetName() << " attach shm..." << std::endl;
        return attaddr;
    }
3.5.2.2 进行shm的取消挂接
cpp 复制代码
    void Dattach(const char* addr)
    {
        if(addr == nullptr) return;
        shmdt(addr);
        std::cout << "name: " << GetName() << "dis attach shm..." << std::endl;
    }
3.5.2.3 进行效果的演示

刚开始的时候,我们会发现在共享内存的页面没有任何数字的变化,因为共享内存的全西安没有关闭,我们需要在创建共享内存的时候,为共享内存的权限设为可以进行操作。

cpp 复制代码
// 修改共享内存的代码:
_shmid = Shmget(_key, 1024, IPC_CREAT | IPC_EXCL | 0x666);

当我们开始实行代码后,会发现这个共享内存的挂接数量从0到1到2,最后,又逐渐减少。注意: 将共享内存段与当前进程脱离不等于删除共享内存,只是取消了当前进程与该共享内存之间的联系。

在服务器端和客户端一起进行挂接,下面是现象:

3.6 对shm的完成封装

cpp 复制代码
#pragma once

#include <iostream>
#include <unistd.h>
#include <errno.h>
#include <cstdio>
#include <string>
#include <sys/types.h>
#include <cstdlib>
#include <cstring>
#include <cstdio>
#include <sys/ipc.h>
#include <sys/shm.h>

#define Creater 1
#define User 2
const std::string pathname = "/home/hrx/code/111/lesson3/shm";
const int proj_id = 0x66;

class Shm
{
public:
    key_t GetKeyCommon()
    {
        key_t k = ftok(_pathname.c_str(), _proj_id);

        if (k < 0)
        {
            perror("GetCommonKey");
        }
        return k;
    }

    int Shmget(int key, int size, int flag)
    {
        int shmid = shmget(key, size, flag);
        if (shmid < 0)
        {
            perror("shmid");
        }
        return shmid;
    }

public:

    int getkey()
    {
        return _key;
    }

    int getshmid()
    {
        return _shmid;
    }

    std::string GetName()
    {
        if(_who == Creater) return "Creater";
        else if(_who == User) return "User";
        else return "None";
    }

    std::string ToHex(int u)
    {
        char buff[128];
        snprintf(buff, sizeof buff, "0x%x", u);
        return buff;
    }

    Shm(const std::string &pathname, int proj_id, int who)
        : _pathname(pathname), _proj_id(proj_id), _who(who)
    {
        _key = GetKeyCommon();
        if (who == Creater)
            GetShmCeaterUse();
        else
            GetShmForUse();
        _addr = Attach(); // 直接进行挂接
    }

    bool GetShmCeaterUse()
    {
        if (_who == Creater)
        {
            _shmid = Shmget(_key, 1024, IPC_CREAT | IPC_EXCL | 0666);
            std::cout << "key:" << _key << ' ' << "shmid:" << _shmid << std::endl;
            if (_shmid > 0)
                return true;
        }
        return false;
    }

    bool GetShmForUse()
    {
        if (_who == User)
        {
            _shmid = Shmget(_key, 1024, IPC_CREAT);
            std::cout << "key:" << _key << ' ' << "shmid:" << _shmid << std::endl;
            if (_shmid > 0)
                return true;
        }
        return false;
    }

    ~Shm()
    {
        if (_who == Creater)
        {
            // 进行共享库的删除
            int res = shmctl(_shmid, IPC_RMID, nullptr);
        }
        std::cout << "Shm remove done..." << std::endl;
    }

    void* Addr()
    {
        return _addr;
    }

    void Zero()
    {
        if(_addr)
            memset(_addr, 0, sizeof _addr);
    }

    void* Attach()
    {
        if(_addr != nullptr) Dattach((char*)_addr); 
        void* attaddr = shmat(_shmid, nullptr, 0);
        if(attaddr == nullptr)
        {
            perror("attaddr");
        }
        std::cout << "name: " << GetName() << " attach shm..." << std::endl;
        return attaddr;
    }

    void Dattach(const char* addr)
    {
        if(addr == nullptr) return;
        shmdt(addr);
        std::cout << "name: " << GetName() << "dis attach shm..." << std::endl;
    }

private:
    key_t _key;
    int _who;
    int _shmid;
    void* _addr;
    const std::string _pathname;
    const int _proj_id;
};

四、进行进程之间的通信

4.1 直接进行通信

在将服务端与客户端进行挂接成功后,我们要来进行通信,将共享内存看成数组来进行通信:

服务端:

cpp 复制代码
    Shm shm(pathname, proj_id, Creater);
    char* shmaddr = (char*)shm.Addr();
    while (true)
    {
        std::cout << shmaddr << std::endl;
        sleep(1);
    }

客户端

cpp 复制代码
    Shm shm(pathname, proj_id, User);
    shm.Zero();
    char* shmaddr = (char*)shm.Addr();
    sleep(3);
    char ch = 'a';
    while (ch <= 'z')
    {
        shmaddr[ch - 'a'] = ch;
        ch++;
        sleep(2);
    }

4.2 共享内存的一些知识点

在进行共享内存的使用时,共享内存不提供任何保护机制,数据不一致问题。我在写一个字符串,但是还没有写完就被读取了一半,造成了数据不一致问题。我们在写共享内存没有使用任何系统调用,共享内存是进程间通信速度最快的,因为共享内存大大地减少了拷贝次数

4.3 保护共享内存

管道提供了保护措施,所以,我们可以建立一个管道,将数据放在管道中,当写完后,直接将数据发送给另一个进程,共享内存的容量大小:如果容量大小为4097,共享内存的大小可以i自己随意定制,但是建议共享内存的大小为4096的整数倍。,如果不是整数倍,操作系统会给你整数倍的容量。

cpp 复制代码
    // 服务器端
    Shm shm(pathname, proj_id, Creater);
    char* shmaddr = (char*)shm.Addr();

    // 2.创建管道
    NamePipe fifo(comepath, Creater);

    fifo.openforread();

    while (true)
    {
        std::string tmp;
        fifo.ReadNamePipe(&tmp);
        std::cout << shmaddr << std::endl;
    }
cpp 复制代码
    // 客户端
    NamePipe fifo(comepath, User);
    fifo.operforwrite();
    char ch = 'a';
    while (ch <= 'z')
    {
        std::string tmp = "once";
        fifo.WriteNamePipe(tmp);
        shmaddr[ch - 'a'] = ch;
        ch++;
        sleep(2);
    }

五、共享内存与管道的对比

当共享内存创建好后就不再需要调用系统接口进行通信了,而管道创建好后仍需要read、write等系统接口进行通信。实际上,共享内存是所有进程间通信方式中最快的一种通信方式。

我们先来看看管道通信:

从这张图可以看出,使用管道通信的方式,将一个文件从一个进程传输到另一个进程需要进行四次拷贝操作:

  • 服务端将信息从输入文件复制到服务端的临时缓冲区中。
  • 将服务端临时缓冲区的信息复制到管道中。
  • 客户端将信息从管道复制到客户端的缓冲区中。
  • 将客户端临时缓冲区的信息复制到输出文件中。

我们再来看看共享内存通信:

从这张图可以看出,使用共享内存进行通信,将一个文件从一个进程传输到另一个进程只需要进行两次拷贝操作:

  • 从输入文件到共享内存。
  • 从共享内存到输出文件。

所以共享内存是所有进程间通信方式中最快的一种通信方式,因为该通信方式需要进行的拷贝次数最少。

但是共享内存也是有缺点的,我们知道管道是自带同步与互斥机制的,但是共享内存并没有提供任何的保护机制,包括同步与互斥。

相关推荐
A小辣椒16 小时前
TShark:Wireshark CLI 功能
linux
A小辣椒20 小时前
TShark:基础知识
linux
AlfredZhao1 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
大树883 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质3 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式