【Linux】第三十四站:共享内存

文章目录

一、直接原理

我们知道进程间通信的本质是:先让不同的进程,看到同一份资源

如下图示,这些我们都还是知道的,即有两个进程,分别有其对应的task_struct和对应的进程地址空间,通过页表可以转换到物理内存上

如下所示,如果我们在物理内存上创建一个共享区,然后通过页表映射虚拟地址,然后返回虚拟地址的起始地址

同理右侧的进程也让它这样做,我们就可以实现让不同的进程看到同一份资源了

这就是共享内存

我们这个具体可以分为三步

  1. 申请内存
  2. 将这块内存挂接到进程地址空间
  3. 返回首地址

那么如果我们要释放这个共享内存呢?

我们首先需要去掉关联

然后释放共享内存

那么上面的操作都是进程直接去做的吗?

当然不是的,必须直接由操作系统来做。

这个过程是由需求方给执行方提供一个系统调用

也就是操作系统要给我们用户提供一个系统调用来做的

操作系统要不要管理所有的共享内存呢???

当然要!

先描述,在组织

要用内核结构体去描述共享内存

二、 代码

1.系统调用接口

我们首先要申请一块共享内存

所用到的系统调用接口是这个

cpp 复制代码
 #include <sys/ipc.h>
 #include <sys/shm.h>
 int shmget(key_t key, size_t size, int shmflg);

关于这个系统调用,下面是参数的介绍

对于第二个参数size

他是创建共享内存的大小 (问题一)

单位是字节
对于它的返回值

如果成功,他会返回一个共享内存的标识,如果失败,返回-1 (问题二)
对于第三个参数shmflg

我们知道,我们这个共享内存创建出来了以后,就不需要再次创建了,只需要获取即可

所以就注定了我们需要如何创建,如何获取

如果需要关注的选项就是下面两个

IPC_CREAT(单独使用)的意思是创建一个共享内存,如果这个共享内存存在,直接获取,如果不存在就创建并返回他

IPC_CREAT | IPC_EXCL(两个一起使用):如果申请的共享额你存不存在,就创建,如果存在就出错返回。它可以确保如果我们申请成功了,这个共享内存一定是一个新的

IPC_EXCL不单独使用

(在这里我们还涉及一个权限问题三,稍后提出)

那么我们这里还存在一个问题,我们如何知道这个共享内存存在还是不存在呢?怎么保证让不同的进程看到同一个共享内存呢?(问题零)这就与key相关了


谈谈key:

  1. 这个key是一个数字,这个数字几,不重要,关键在于它必须在内核中具有唯一性,能够让不同的进程进行唯一性标识

  2. 第一个进程可以通过key创建共享内存,第二个之后的进程,只要拿着同一个key,就可以和第一个进程看到同一个共享内存了!

  3. 对于一个已经创建好的共享内存,那么key在哪?

    key在共享内存的描述对象中!

  4. 第一次创建的时候,必须有一个key了,这个key怎么有呢?

  5. key --类似于 --路径 ,具有唯一性

先来回答key的第四个问题

有一个接口

cpp 复制代码
#include <sys/types.h>       
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);

这个接口中它其实就是一套算法,由pathname,和proj_id进行了数值计算

而这两个参数也是由用户自由指定的

最后的返回值就是一个冲突概率比较低的key

如果冲突了也就是失败了,我们需要调整一下两个参数

那么为什么这个接口要由操作系统来提供,并且还有可能冲突呢?

因为同一个函数中,使用同一个参数可以产生同一个key。我们这就可以确保两个进程具有一样的key了

2.创建共享内存

cpp 复制代码
#include "comm.hpp"
int main()
{
    int shimid = GetShareMem();
    
    sleep(20);

    log(Info, "process quit...");

    return 0;
}

comm.hpp

cpp 复制代码
#ifndef __COMM_HPP__
#define __COMM_HPP__

#include <iostream>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string>
#include <cstring>
#include "log.hpp"
using namespace std;

const string pathname = "/home/jby_1";
const int proj_id = 0x6666;
const int size = 4096;
Log log;

key_t GetKey()
{
    key_t key = ftok(pathname.c_str(), proj_id);
    if(key < 0)
    {
        log(Fatal, "ftok error: %s", strerror(errno));
        exit(1);
    }
    log(Info, "ftok success, key is : %d", key);
    return key;
}

int GetShareMem() 
{   
    key_t key = GetKey();
    int shmid = shmget(key, size, IPC_CREAT|IPC_EXCL);
    if(shmid < 0)
    {
        log(Fatal, "creat share memory error : %s", strerror(errno));
        exit(2);
    }
    log(Info, "creat share memory success, shimid : %d", shmid);
    return shmid;
}       



#endif

如上代码的运行结果为

那么这个key和shmid有什么区别呢?

这个key是操作系统内标定唯一性的

而shmid是只在进程内,用来表示资源唯一性的

即一个在用户层,一个在内核层

当我们再次运行这个进程的时候

我们会发现他会说,文件已经存在了。

我们可以用下面这个指令去查看系统当中的共享内存

ipcs -m

这个shmid和key都是与我们前面进程所运行的结果一致的,这就说明这个共享内存就是由这个进程所创建的

而我们此时进程早已退出,这就说明如果我们不主动的关闭掉这个共享内存,那么它是不会被关的

即:共享内存的生命周期是随内核的 !

用户不主动关闭,共享内存会一直存在。除非内核重启/用户释放。

如果我们要释放掉这个共享内存,那么用下面的指令

bash 复制代码
ipcrm -m shmid值

运行结果为

这是因为key用于内核层,而shmid是用户层的。只有在内核当中进行对比的时候,才会使用key。在用户层统一使用shmid

我们删掉这块内存,之后重新运行就可以了

这里这个perms是权限,它其实也是一个文件,我们可以为他设置对应的权限

nattch是关联的意思,意思是当前有几个进程与这块共享内存是关联的

关于这个共享内存的权限问题,我们可以在这里进行设置

如下就是运行结果了

总之。上面我们以及该创建好了共享内存,不过我们还需要的是获取

3.获取共享内存

现在我们前面的代码已经被改为了如下

cpp 复制代码
#ifndef __COMM_HPP__
#define __COMM_HPP__

#include <iostream>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string>
#include <cstring>
#include "log.hpp"
using namespace std;

const string pathname = "/home/jby_1";
const int proj_id = 0x6666;
const int size = 4097;
Log log;

key_t GetKey()
{
    key_t key = ftok(pathname.c_str(), proj_id);
    if(key < 0)
    {
        log(Fatal, "ftok error: %s", strerror(errno));
        exit(1);
    }
    log(Info, "ftok success, key is : 0x%x", key);
    return key;
}

int GetShareMemHelper(int flag) 
{   
    key_t key = GetKey();
    int shmid = shmget(key, size, flag);
    if(shmid < 0)
    {
        log(Fatal, "creat share memory error : %s", strerror(errno));
        exit(2);
    }
    log(Info, "creat share memory success, shimid : %d", shmid);
    return shmid;
}       

int CreateShm()
{
    return GetShareMemHelper(IPC_CREAT|IPC_EXCL|0666);
}

int GetShm()
{
    return GetShareMemHelper(IPC_CREAT);
} 

#endif

如下所示,我们创建了一个4097字节大小的共享内存

但是其实我们更加建议创建一个4KB倍数的共享内存大小。

因为虽然我们按4097申请成功了,但是实际上操作系统给的是4096*2的大小,多余的部分不仅给了,还没法用。相当于浪费掉了。

4.将共享内存挂接到虚拟进程地址空间中

这里有一个shmat接口

cpp 复制代码
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);

第一个参数好说,我们知道就是我们刚刚的共享内存的shmid

第二个参数shmaddr,代表的含义是我们想将共享内存挂接到哪个位置。即共享区的哪个位置,我们一般可以设置为nullptr,让系统去决定挂到哪里去了

第三个参数就是该共享内存的权限,比如只读等等,当然我们也可以设置为0,就是按照默认的来

这个返回值void*比较像我们以前的malloc,malloc得到是一块虚拟地址,只有当读写的时候才发生缺页中断,才申请内存

我们现在用如下代码进行测试

cpp 复制代码
#include "comm.hpp"
int main()
{
    int shmid = CreateShm();
    log(Info, "Create share success");
    
    sleep(5);

    char* shmaddr = (char*)shmat(shmid, nullptr, 0);
    log(Info, "attach shm success");
  
    sleep(5);
    return 0;
}

如下所示,前五秒连接数为0,随后五秒为1,随后变为0。但是共享内存一直存在

5.去掉关联

我们前面已经有关联的接口了,现在我们不想等进程退出,就可以进行去关联,这个也是有对应的接口的

cpp 复制代码
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);

这里这个参数其实就是我们曾经使用shmat关联函数所得到的虚拟地址

这个接口是比较类似于之前free接口的

那么我们这个是怎么知道我们要释放多少呢?

所以这里一定隐藏着我们没有看到的数据。malloc也是一样的

malloc其实会比我们要申请的内存多一点点的。用于存储一些我们看不到的数据。所以malloc其实更适合用于申请大空间,因为这样影响比较小。

我们一般把这些多申请的空间叫做cookie

这个接口的返回值是成功为0,失败为-1

我们用如下代码,运行结果为如下:

cpp 复制代码
#include "comm.hpp"
int main()
{
    int shmid = CreateShm();
    log(Info, "Create share success");
    sleep(5);
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);
    log(Info, "attach shm success, shmaddr: 0x%x", shmaddr);
    sleep(5);
    shmdt(shmaddr);
    log(Info, "detach shm success, shmaddr: 0x%x", shmaddr);
    sleep(5);
    return 0;
}

6.释放共享内存

我们前面所用的都是指令去删除掉共享内存,但是其实系统调用也是有删除共享内存的接口的

cpp 复制代码
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

第一个参数我们还是比较清楚的就是共享内存的id

第三个参数就是这样一个结构体

这些就是一些属性。

这个结构体里面的属性是我们内核当中管理共享内存的一个子集。也就是可以获取共享内存里面的属性

第二个参数中

我们只关心这个

它的作用是将共享内存标记为被删除的。

而我们删除的时候是不关心它的属性的,所以属性直接设置为nullptr

对于返回值

成功为0,失败为-1

我们代码如下

cpp 复制代码
#include "comm.hpp"
int main()
{
    int shmid = CreateShm();
    log(Info, "Create share success");
    sleep(5);
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);
    log(Info, "attach shm success, shmaddr: 0x%x", shmaddr);
    sleep(5);
    shmdt(shmaddr);
    log(Info, "detach shm success, shmaddr: 0x%x", shmaddr);
    sleep(5);
    shmctl(shmid, IPC_RMID, nullptr);
    log(Info, "destory shm success, shmaddr: 0x%x", shmaddr);
    return 0;
}

运行结果为

7.两个进程一起共享

我们让进程b的代码为如下

cpp 复制代码
#include "comm.hpp"
int main()
{
    int shmid = GetShm();
    log(Info, "Create share success");
    sleep(3);
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);
    log(Info, "attach shm success, shmaddr: 0x%x", shmaddr);
    sleep(3);
    shmdt(shmaddr);
    log(Info, "detach shm success, shmaddr: 0x%x", shmaddr);
    sleep(3);
    return 0;
}

进程a如下

cpp 复制代码
#include "comm.hpp"
int main()
{
    int shmid = CreateShm();
    log(Info, "Create share success");
    sleep(3);
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);
    log(Info, "attach shm success, shmaddr: 0x%x", shmaddr);
    sleep(3);
    shmdt(shmaddr);
    log(Info, "detach shm success, shmaddr: 0x%x", shmaddr);
    sleep(3);
    shmctl(shmid, IPC_RMID, nullptr);
    log(Info, "destory shm success, shmaddr: 0x%x", shmaddr);
    return 0;
}

最终运行结果为如下

8.通信

截止到现在,我们前面还没有开始通信

我们前面所做的一切都是让不同的进程看到同一份资源

我们现在可以实现一个最简单的通信

下面是进程a的代码

cpp 复制代码
#include "comm.hpp"
int main()
{
    int shmid = CreateShm();
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);

    //ipc code
    while(true)
    {
        cout << "process assess a message: " << shmaddr << endl;
        sleep(1);
    }



    shmdt(shmaddr);
    shmctl(shmid, IPC_RMID, nullptr);
    return 0;
}

下面是进程b的代码

cpp 复制代码
#include "comm.hpp"
int main()
{
    int shmid = GetShm();
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);

    //ipc code
    while(true)
    {
        char buffer[1024];
        cout << "Please Enter@";
        fgets(buffer, sizeof(buffer), stdin);
        memcpy(shmaddr, buffer, strlen(buffer) + 1);
    }


    shmdt(shmaddr);
    return 0;
}

运行结果为

当然上面的代码我们对进程b还加了一个缓冲区,其实我们连缓冲区都不需要

cpp 复制代码
#include "comm.hpp"
int main()
{
    int shmid = GetShm();
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);

    //ipc code

    //一旦有了共享内存,挂接到字节的地址空间中,我们直接把它当成我们的内存空间来用即可!
    //不需要系统调用
    while(true)
    {
        cout << "Please Enter@";
        fgets(shmaddr, 4096, stdin);
    }


    shmdt(shmaddr);
    return 0;
}

对于进程b

一旦有了共享内存,挂接到字节的地址空间中,我们直接把它当成我们的内存空间来用即可!

不需要系统调用

对于进程a

一旦有人把数据写入到共享内存,其实我们立马就能看到了

不需要经过系统调用,就能看到数据了

三、共享内存的特性

  1. 共享内存没有同步互斥之类的保护机制

  2. 共享内存是所有进程间通信中,速度最快的!

因为拷贝少。而管道的方式,是要先将数据写入到管道中,然后再从管道读出来。它这种方式直接写入到内存当中了,直接用即可

  1. 共享内存内部的数据,由用户自己维护!

四、共享内存的属性

如下所示,是我们前面所提及的共享内存的一些属性

在第一个结构体中,最重要的字段是第一个shm_perm,这个还是一个结构体,也就是下面的这个。其他的字段后面都有解释

在这个第二个结构体中,第一个就是key。所以我们应用层中生成的key,最后一定会被写入到内核中。里面的这个mode就是共享内存的权限问题

我们可以直接去看看这些属性

cpp 复制代码
#include "comm.hpp"
int main()
{
    int shmid = CreateShm();
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);

    struct shmid_ds shmds;
    //ipc code
    while(true)
    {
        cout << "process assess a message: " << shmaddr << endl;;
        sleep(1);
        shmctl(shmid, IPC_STAT, &shmds);
        cout << "shm size: " << shmds.shm_segsz <<endl;
        cout << "shm nattch: " << shmds.shm_nattch <<endl;
        printf("shm __key: 0x%x \n" ,shmds.shm_perm.__key);
        cout << "shm mode: " << shmds.shm_perm.mode <<endl;

    }
    shmdt(shmaddr);
    shmctl(shmid, IPC_RMID, nullptr);
    return 0;
}

运行结果为

五、同步

我们的代码没有同步机制。但是管道有。

我们可以用管道的方式。来进行同步

进程a代码

cpp 复制代码
#include "comm.hpp"
int main()
{
    Init i;
    
    int shmid = CreateShm();
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);


    int fd = open(FIFO_FILE, O_RDONLY);

    struct shmid_ds shmds;

    //ipc code
    while(true)
    {
        char c;
        ssize_t n = read(fd, &c, 1);
        if(n == 0) break;
        else if(n < 0) break;
        cout << "process assess a message: " << shmaddr << endl;;
        sleep(1);
        shmctl(shmid, IPC_STAT, &shmds);
        cout << "shm size: " << shmds.shm_segsz <<endl;
        cout << "shm nattch: " << shmds.shm_nattch <<endl;
        printf("shm __key: 0x%x \n" ,shmds.shm_perm.__key);
        cout << "shm mode: " << shmds.shm_perm.mode <<endl;

    
    shmdt(shmaddr);
    shmctl(shmid, IPC_RMID, nullptr);
    close(fd);
    return 0;
}

进程b代码

cpp 复制代码
#include "comm.hpp"
int main()
{


    int shmid = GetShm();
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);

    int fd = open(FIFO_FILE, O_WRONLY);


    //ipc code

    //一旦有了共享内存,挂接到字节的地址空间中,我们直接把它当成我们的内存空间来用即可!
    //不需要系统调用
    while(true)
    {
        cout << "Please Enter@";
        fgets(shmaddr, 4096, stdin);
        write(fd,"c", 1);//通知对方

    }


    shmdt(shmaddr);
    close(fd);
    return 0;
}

comm.hpp代码

cpp 复制代码
#ifndef __COMM_HPP__
#define __COMM_HPP__

#include <iostream>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string>
#include <cstring>
#include "log.hpp"

using namespace std;

const string pathname = "/home/jby_1";
const int proj_id = 0x6666;
const int size = 4097;
Log log;


#define FIFO_FILE "./myfifo"
#define MODE 0664

enum 
{
    FIFO_CREATE_ERR = 1,
    FIFO_DELETE_ERR,
    FIFO_OPEN_ERR
    
};

key_t GetKey()
{
    key_t key = ftok(pathname.c_str(), proj_id);
    if(key < 0)
    {
        log(Fatal, "ftok error: %s", strerror(errno));
        exit(1);
    }
    log(Info, "ftok success, key is : 0x%x", key);
    return key;
}

int GetShareMemHelper(int flag) 
{   
    key_t key = GetKey();
    int shmid = shmget(key, size, flag);
    if(shmid < 0)
    {
        log(Fatal, "creat share memory error : %s", strerror(errno));
        exit(2);
    }
    log(Info, "creat share memory success, shimid : %d", shmid);
    return shmid;
}       

int CreateShm()
{
    return GetShareMemHelper(IPC_CREAT|IPC_EXCL|0666);
}

int GetShm()
{
    return GetShareMemHelper(IPC_CREAT);
} 

class Init
{
public:
    Init()
    {
        //创建一个管道
        int n = mkfifo(FIFO_FILE, MODE);
        if(n == -1) 
        {
            perror("mkfifo");
            exit(FIFO_CREATE_ERR);
        }
    }
    ~Init()
    {
        //关闭信道
        int m = unlink(FIFO_FILE);
        if(m == -1)
        {
            perror("unlink:");
            exit(FIFO_DELETE_ERR);
        }
    }

};

#endif

运行结果为,如下所示,我们实现了同步

相关推荐
微服务商城技术分享几秒前
通过Docker实现openGauss的快速容器化安装
运维·docker·容器
星光樱梦6 分钟前
24. 正则表达式
c++
fathing7 分钟前
c# 调用c++ 的dll 出现找不到函数入口点
开发语言·c++·c#
运维佬36 分钟前
在 Linux 系统上部署 Apache Solr
linux·apache·solr
编程墨客1 小时前
第03章 文件编程
linux·运维·服务器
命里有定数1 小时前
windows工具 -- 使用rustdesk和云服务器自建远程桌面服务, 手机, PC, Mac, Linux远程桌面 (简洁明了)
linux·运维·服务器·windows·ubuntu·远程工作
cleveryuoyuo1 小时前
进程的程序替换exec*函数和shell实现
linux·服务器
运维佬1 小时前
nginx配置负载均衡详解
运维·nginx·负载均衡
微凉的衣柜1 小时前
使用 VS Code 远程连接时解决 OpenSSL 版本不匹配及权限问题
服务器·vscode·ubuntu
hope_wisdom1 小时前
C++网络编程之SSL/TLS加密通信
网络·c++·ssl·tls·加密通信