Linux:共享内存介绍(进程间通信)

共享内存原理介绍

system V 共享内存是两个独立的进程之间完成通信的一个物理内存块。

直接看概念是带蒙的,下面来讲讲其中的缘由:

一个正在运行进程,由下面几个方面构成:进程的 PCB、进程地址空间页表。进程的数据通过页表映射到物理内存特定的位置。每个进程通过页表的映射,在物理内存的位置都会不一样。由此才能保证进程的独立性!

为了保证两个独立的进程能够进行通信,就要让这两个进程看到同一块空间。匿名管道和命名管道都是通过创建内存级的文件,来让两个独立的进程看到同一份资源。

那么共享内存又是如何操作的呢?

共享内存就是通过系统调用接口,在物理内存中开辟一块空间。在这里假设有两个独立的进程分别为:进程A 和 进程B。

前面提到,每个进程都有属于自己的进程地址空间。这个进程地址空间包含有:栈区、堆区、代码区… 在这里主要说一下在栈区 和 堆区之间的 共享区

为了让 进程A 和 进程B 能够通信,可以将创建的共享内存,通过页表的映射方式分别映射到 进程A 和 进程B 的共享区

在这里插入图片描述
此时,共享内存的地址就通过页表映射到进程地址空间的共享区,用户就可以通过共享区拿到对应的起始地址。这样就可以使 进程A 和进程B 看到同一份资源,进而就可以对两个独立的进程实现通信了

进程完成通信后,共享内存又是如何释放资源的呢?

对于创建共享内来说,释放空间是比较简单的。只需要找到进程,进程再通过页表映射到物理内存开辟的共享内存的地址进行修改,然后再对这块空间进行释放即可。

因此使用共享内存,通常这几步:创建共享内存、将独立的进程对共享内存进行关联通信、取消进程与共享内存的关联、释放共享内存

共享内存系统调用接口

shmget 创建共享内存段

创建共享内存需要用到系统调用接口:shmget

使用这个接口需要包含头文件:#include<sys/ipc.h>#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);

参数介绍:

  • key:用于唯一标识共享内存段(介绍另一个接口时,详细介绍这个参数)
  • size:创建共享内存段的大小,以字节为单位
  • shmflg:标志位,常用的标志位有两个 IPC_CREATIPC_EXCL
  • 返回值:成功创建共享内存返回共享内存的 描述符(后续介绍);失败返回 -1

IPC_CREAT 标志位表示:创建一个共享内存段。如果共享内存段不存在,创建一个新的共享内存段;如果共享内存段存在,返回这个共享内存段。

IPC_EXCL 通常要配合 IC_CREAT 一起使用,加上 IPC_EXCL 标志位表示:创建一个新的共享内存段。如果这个共享内存段不存在,创建一个新的共享内存段;如果这个共享内存段存在,直接出错返回。

下面再来介绍,第二个参数唯一键: key

ftok 生成唯一键 key

这个key值一般是通过 ftok 函数来生成的,ftok 函数内部会使用一些算法生成唯一的 key 值。

使用 ftok函数需要包含头文件:#include <sys/types.h>#include <sys/ipc.h>

key_t ftok(const char* pathname, int proj_id);

参数介绍:

  • pathname:一个已存在的文件路径名。这个文件用于生成键,但其内容并不重要
  • proj_id:一个非负整数,作为项目标识符,与 pathname 一起用于生成键
  • 返回值:设置成功返回唯一键 key;失败返回 -1

进程A 和 进程B 可以通过共享内存进行通信。在操作系统中,存在这么多进程,是不是都是可以通过共享内存进行通信呢?

答案是可以的。一对对的进程都可以进行共享内存进行通信,这样将会开辟多个共享内存。针对如此多的共享内存,操作系统是需要对其进行做管理的。为了管理如此多的共享内存,操作系统会先对这些共享内存进行描述,创建属于共享内存结构体对象。假设创建:struct shm 结构体,每个结构体内都存储着共享内存的属性(指向共享内存的指针、共享内存大小、创建者、权限是多少、当前有多少进程正在使用这个共享内存等等),最重要的是这些属性中会存在着 struct shm* 指针,指向下一个共享内存的结构体属性。正是有了这些结构体指针,操作系统对共享内存的 删除 和 更改 就变成对 链表 的 增删查改!完成组织的操作。

因此,可以这样说:共享内存 == 共享内存的内核数据结构 + 真正在物理内存中开辟的内存空间

但是接下来就会出现这样的情况:
在这里插入图片描述

在物理内存中,存在着多个共享内存,操作系统为了管理这些共享内存会创建对应的 struct shm 结构体,形成链表的方式组织管理起来。面对如此多的共享内存,进程A 为了和 进程B 进行通信,就必须找到同一块资源。也就是要让 进程A 和 进程B 达成共识性的东西

这也是为什么要使用到 ftok 函数了。 进程A 和 进程B 约定,调用 ftok 函数一起传入一个两个进程都知道的文件路径,然后再传入一个项目标识符,最后生成一个唯一键 key

在这里假设 进程A 来创建一个共享内存(一般是谁创建谁来设置 唯一键 key),创建好一个共享内存后,进程A 将唯一键 key 设置到共享内存结构体的属性中。由于 进程A 和 进程B 是有约定的,所以在 进程B 使用 ftok 函数的时候,也会生成同一个 key 值。通过这个key值,进程A 和 进程B 就可以在诸多共享内存中找到 进程A 创建的共享内存,通信就可以开始。
在这里插入图片描述

因此,进程A 和 进程B 内部都要调用 ftok 函数来生成这样的唯一键,才能找到对应的共享内存

提了这么多理论上的点,下面来实操演示下:

  1. 创建三个文件:server.cc、 client.cc 、comm.hpp。
    comm.hpp 文件用来实现一些函数,为两个源文件提供使用:
#pragma once

#include <iostream>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/ipc.h>

//文件路径和项目id可以随意设置,但是要保证通信的进程之间都一样
#define PATHNAME "."  //文件路径
#define PROJID 0x6666 //项目id值

#define NUM 64

key_t getKey()
{
    key_t n = ftok(PATHNAME, PROJID);
    if(n == -1)
    {
        std::cout << errno << ":" << strerror(errno) << std::endl;
        return errno;
    }
    return n;
}

std::string toHex(int x)
{
    char buffer[NUM];
    snprintf(buffer, NUM - 1, "0x%x", x);//将整除x转换为16进制字符串显示

    return buffer;
}
  1. 实现server.cc 和 client.cc 文件,两个文件都包含 comm.hpp文件,分别调用 getKey 函数。检验不同进程之间生成的唯一键是否相同
//server.cc 文件
#include "comm.hpp"

int main()
{   
    //创建唯一键key
    key_t key = getKey();
    std::cout << "key:" << toHex(key) << std::endl;
}
//client.cc 文件
#include "comm.hpp"

int main()
{   
    //创建唯一键key
    key_t key = getKey();
    std::cout << "key:" << toHex(key) << std::endl;
}

编译运行,查看结果:
在这里插入图片描述

共识达成后,接下来就是创建共享内存的时候了。

接下来我会标注每一段代码都在哪个文件实现,防止老铁们迷路。整体代码会在篇尾全部给出。

开始创建共享内存

  1. 在 comm.hpp 文件增加两个函数 :createShm 函数用于创建共享内存、 getShm 函数用于获取共享内存

createShm 函数和 getShm 函数的实现逻辑其实都差不多,最大差异就是创建共享内存的时候是要开辟一个全新的共享内存段。获取函数只需要获取已经存在的共享内存即可。为了防止代码冗余,再实现多一个函数 createShmHelper ,用于createShm 函数和 getShm 函数 调用。

const int gsize = 4096;

int createShmHelper(key_t key, int size, int flag)
{
    int shmid = shmget(key, size, flag);//创建共享内存段
    if(shmid == -1)
    {
        std::cout << errno << strerror(errno) << std::endl;
        exit(2);
    }
    return shmid;
}

int createShm(key_t key, int size)
{
    return = createShmHelper(key, gsize, IPC_CREAT|IPC_EXCL); //创建一块全新的共享内存段
}

int getShm(key_t key, int size)
{
    return = createShmHelper(key, gsize, IPC_CREAT); //获取创建的共享内存块
}
  1. server.cc 文件创建共享内存段,client.cc 文件来获取共享内存段。
//servet.cc文件
#include "comm.hpp"

int main()
{   
    //1. 定义唯一键key
    key_t key = getKey();
    std::cout << "key:" << toHex(key) << std::endl;
    //2.创建共享内存段
    int shmid = createShm(key, gsize);
    std::cout << "shmid:" << toHex(shmid) << std::endl;

    return 0;
}
//client.cc文件
#include "comm.hpp"

int main()
{   
    //1.创建唯一键key
    key_t key = getKey();
    std::cout << "key:" << toHex(key) << std::endl;

    //2.获取共享内存块
    int shmid = getShm(key, gsize);
    std::cout << "shmid:" << toHex(shmid) << std::endl;

    return 0;
}
  1. 编译 server.cc 和 client.cc 文件,运行结果如下:

在这里插入图片描述

如果只是创建共享内存,那么还是比较简单的。但是,当我们再次运行 server 文件时会出现下面这种情况:

在这里插入图片描述

创建的共享内存还存在!共享内存再次被创建之后,共享内存的生命周期不会随着进程的退出而结束。它会一直被维护在操作系统中,直到操作系统退出。要对原来的共享内存先进行释放,才能继续创建共享内存

下面来介绍两个Linux 指令,分别是 :ipcs 查看共享内存段 、ipcrm 删除共享内存段。

指令 ipcs -m 查看共享内存

ipcs -m 指令通常用来查询共享内存段的信息:

ipcs -m

在这里插入图片描述
可以看到,标志位 32768 就是kunkun 这个用户通过 server 进程创建的共享内存段。此时的 server进程已经完全退出了,但是开辟共享内存依旧存在。

那么如何去释放这个空间呢?下面来介绍另一个指令:

指令 ipcrm -m 删除共享内存段

ipcrm -m 共享内存段标志位
  • -m 选项:删除用指定 标志位 的共享内存段

注意:-m 选项后面加上的是共享内存断的标志位,不是唯一键 key

下面将 32768 标志位的共享内存段释放:
在这里插入图片描述

除了指令的方式删除共享内存段,还可以通过系统调用的方式去删除共享内存段

shmctl 控制创建的共享内存

shmctl 函数是Linux中用于控制共享内存操作的一个系统调用函数,通过这个函数可以对共享内存进行 写、读、删除等操作。

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

参数介绍:

  1. shmid 参数:共享内存标识符
  2. cmd 参数:类似于位图,传入控制宏参数,指定shmctl函数对共享内存执行的具体操作
  3. buf 参数:指向 shmid_ds 结构体的指针,shmid_ds 结构体包含了共享内存的各种信息
  4. 返回值:成功返回0;失败返回 -1

这里主要来介绍如何对共享内存进行删除操作,其他像是 cmd 参数中诸多宏的用法,有感兴趣的老铁可以自行去使用使用。

通过系统调用来删除共享内存

删除共享内存,需要用到 shmctl 函数的第二个参数 cmd 的 IPC_RMID

在 comm.hpp 文件增加一个删除的函数,方便 server.cc 文件对共享内存进行删除的操作

//comm.hpp
void delShm(int shmid)
{
    //删除共享内存
    int n = shmctl(shmid, IPC_RMID, nullptr);
    assert(-1);
    (void)n;
    std::cout << "共享内存已经被删除\n";
}

对server.cc 文件内容进行修改:

//server.cc 文件
int main()
{   
    //1. 定义唯一键key
    key_t key = getKey();
    std::cout << "key:" << toHex(key) << std::endl;
    //2.创建共享内存段
    int shmid = createShm(key, gsize);
    std::cout << "shmid:" << toHex(shmid) << std::endl;

    sleep(3);//停顿3秒模拟实现共享内存使用效果

    //删除共享内存
    delShm(shmid);

    return 0;
}

再来编译运行看看:

在这里插入图片描述

共享内存权限问题

前面介绍共享内存的操作都是如何创建、查看 和 删除共享内存,没有提到权限的问题。由于接下来要使用这块共享内存,对此要讲一下关于共享内存权限访问问题:

通过先前的方式创建一个共享内存,仔细观察一下这块共享内存的信息:
在这里插入图片描述
标红处表示:kunkun这个用户创建的的共享内存是没有读写权限的。尽管这块空间是kunkun用户创建,kunkun本身也没有读写权限,是不能使用这块空间的。但是创建这块共享内存的主人是可以对这块空间进行释放的

如何解决呢?

只需要在创建共享内存的时候给上对应的权限即可,修改 comm.hpp 文件:

int createShm(key_t key, int size)
{
    umask(0);//将权限掩码设置为0
    int shmid = createShmHelper(key, gsize, IPC_CREAT|IPC_EXCL| 0666); //创建一块新的共享内存段,赋予666的权限,0666表示八进制
    return shmid;
}

共享内存的权限设置犹如文件权限那般,受到权限掩码的影响(要设置的权限 & ~umask 得到最终权限)

编译运行看看结果:
在这里插入图片描述

创建的共享内存权限已经被设置为 666

关联/去关联共享内存

共享内存的创建、释放都有了,接下来的问题就是如何关联共享内存。

来介绍两个接口 shmatshmdt,这个接口是用来 关联去关联 一块共享内存空间。使用这两个接口需要包含头文件:#include<sys/types.h>#include<sys/shm.h>

先来介绍第一个接口 shmat ,用于关联共享内存:

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

at 是 attach 单词缩写,表示关联的意思。

参数作用:

  • shmid:要关联的共享内存
  • shmaddr:将共享内存挂接到进程地址空间的确定位置
  • shmflg:设置共享内存读写
  • 返回值:挂接成功后,返回共享内存映射在进程地址空间的起始地址(虚拟地址)

一般的,shmaddr 参数我们都不会自己去手动设置,直接传 nullptr 让OS去帮我们分配;shmflg 参数 是通过传标志位的方式来控制这块共享内存读写,例如 :SHM_RDONLY 宏表示只读。没有其他要求,在这里我们传 0 即可。

shmat 的系统接口的返回值 void* 很像C语言的 malloc 函数的返回值,我们可以像 malloc 那般去使用这个返回值。

再来介绍一下 shmdt 接口,将进程 去关联 共享内存:

int shmdt(const void *shmaddr);

dt 是单词 detach 缩写,表示分离的意思。

参数作用:

  • shmaddr:表示已经被挂载到进程地址空间的共享内存的起始地址
  • 返回值:去关联成功返回0,失败返回-1

下面来编写代码,将两个独立的进程 关联 和 去关联到同一块共享内存:

先在 comm.hpp 文件中去编写两个函数:attachShm关联共享内存、detachShm 去关联共享内存

//comm.hpp
//关联
char* attachShm(int shmid)
{
    char* start = (char*)shmat(shmid, nullptr, 0);
    if(start == nullptr)
    {
        exit(errno);
    }
    return start;
}

//去关联
void detachShm(char* start)
{
    int n = shmdt(start);
    if(n == -1) exit(errno);
}

通过这两个接口,server进程 和 client进程就可以关联到同一块共享内存进行通信了。下面来编写这两个进程的代码:

//server.cc
int main()
{   
    //1. 定义唯一键key
    key_t key = getKey();
    std::cout << "key:" << toHex(key) << std::endl;
    //2.创建共享内存段
    int shmid = createShm(key, gsize);
    std::cout << "shmid:" << toHex(shmid) << std::endl;

    //将当前进程与共享内存关联起来
    char* start = attachShm(shmid);

    sleep(15);//停顿5秒模拟实现共享内存使用效果

    //将当前进程与共享内存去关联
    detachShm(start);

    //删除共享内存
    delShm(shmid);

    return 0;
}
//client.cc
int main()
{   
    //1.创建唯一键key
    key_t key = getKey();
    std::cout << "key:" << toHex(key) << std::endl;

    //2.获取共享内存块
    int shmid = getShm(key, gsize);
    std::cout << "shmid:" << toHex(shmid) << std::endl;

    //关联共享内存
    char* start = attachShm(shmid);

    //使用共享内存
    sleep(10);

    //去关联
    detachShm(start);

    return 0;
}

先将 server进程 跑起来,创建共享内存,再将 client进程跑起来;由于,server进程运行的时间比 client进程 运行的时间长,client进程会先退出,通过先后顺序来观察进程关联共享内存的现象。

下面将两个进程跑起来:

  1. server创建共享内存,并且与这个共享内存关联起来:
    在这里插入图片描述2. client 进程运行起来关联到这块共享内存,此时的关联数变成2:
    在这里插入图片描述3. 经过一段时间后,client 进程会先去关联再退出,关联数会变成1:
    在这里插入图片描述4. 再后来就是 server进程去关联,关联数会变成1,最后 server进程释放这块共享内存:
    在这里插入图片描述

封装处理

创建共享内存、关联共享内存、去关联共享内存、删除共享内存,对于创建与删除 都是由 serve进程在处理,关联与去关联操作server进程与client 进程都是在重复操作。

对此可以将上述操作都封装成一个类,利用构造函数去创建与关联共享内存析构函数用来去关联与删除共享内存的操作。在这里要注意一下,创建与删除共享内存的操作只需要一个进程负责即可。

在 comm.hpp 文件中,定义两个宏,用于标识谁来创建与删除共享内存;实现 Init 类,去封装上面实现的 创建、删除、关联、去关联 函数接口:

#define SERVER 0
#define CLIENT 1

class Init
{
public:
    Init(int type)
        : _type(type)
    {
        // 获取唯一键
        key_t key = getKey();
        std::cout << "key:" << toHex(key) << std::endl;
        if (_type == SERVER) // 符合SERVER创建
        {
            // 创建共享内存
            _shmid = createShm(key, gsize);
            std::cout << "进程创建共享内存成功,shmid:" << toHex(_shmid) << std::endl;
        }
        else
        {
            // 获取共享内存
            _shmid = getShm(key, gsize);
            std::cout << "进程已经获取到共享内存:shmid:" << toHex(_shmid) << std::endl;
        }
        // 关联共享内存
        _start = attachShm(_shmid);
    }
    char* getStart() { return _start; } //用于获取共享内存地址
    ~Init()
    {
        // 去关联共享内存
        detachShm(_start);
        if (_type == SERVER)
        {
            // 删除共享内存
            delShm(_shmid);
        }
    }
private:
    int _type;    // 用于标识谁来创建、删除共享内存
    int _shmid;   // 共享内存编号
    char *_start; // 共享内存地址(虚拟地址)
};

对此,我们在 server.cc 和 client.cc 文件中只需要创建 Init 对象就可以实现共享内存的创建、关联操作;Init 对象生命周期结束,调用析构函数完成对应的共享内存 去关联、删除操作。

有兴趣的小伙伴可以尝试利用下面的一个简单示例,来实现两个独立的进程通过共享内存完成一次通信过程:

client进程 向共享内存中逐个写入 1~26 个字符,server 从共享内存中获取client 写入的字符串并输出到终端。完成写的工作,client 进程写完后会先退出,server进程随后退出。

//server.cc
int main()
{
    Init init(SERVER);
    char *start = init.getStart();

    int n = 0;
    while(n <= 30)
    {
        cout <<"client -> server# "<< start << endl; //读取字符串
        sleep(1);
        n++;
    }
    
 	return 0;
}
//client.cc
int main()
{
    Init init(CLIENT);
    char *start = init.getStart();
    char c = 'A';

    while(c <= 'Z')
    {
        start[c - 'A'] = c;
        c++;
        start[c - 'A'] = '\0';
        sleep(1);
    }
	return 0;
}    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值