【Linux】System V 通信——共享内存

一、原理

首先既然是通信,那么就必须是2个以上的进程进行通信,而且这些进程必须要看到同一份资源,所以假设我们可以在物理内存上开辟一块内存块,然后通过页表分别映射到这些进程的虚拟地址空间,这样这些进程进程就可以通过虚拟地址空间看到同一个物理内存的内存块,实现进程间通信,我们把在物理内存上开辟出来的内存块称之为:共享内存。而把物理内存上的内存块映射到虚拟地址方式称之为:挂接。

二、理解

问题:我们把物理内存上的共享内存,映射到进程的虚拟地址的什么地方?

答:映射到进程虚拟地址的:共享区。

1、共享内存的原理,本质上是一个简化版本的动态库映射。

2、因为共享内存有多个(多个进程间通信),所以 OS 要管理共享内存(防止内存泄漏),怎么管理?答:先描述(结构体),再组织(数据结构),结论:共享内存 = 共享内存结构体 + 共享内存本身。

3、使用共享内存的步骤:1、在物理内存上创建共享内存,2、关联(挂接),3、使用共享内存,4、去关联,5、释放共享内存。

三、共享内存是什么?为什么?怎么用?

是什么?

答:共享内存是采用多个进程,使用虚拟地址空间映射的方式,让不同的进程看到同一个内存块。

为什么?

先了解一下共享内存的接口,再回答这个问题:

首先,共享内存不能有个别进程 new 出来,即使技术上可以实现,但是 OS 不允许,这违规了规则(进程进程间的独立性),故而:共享内存只能由 OS 来申请。

怎么让 OS 申请共享内存:使用系统调用:

参数:

size:创建共享内存的大小,注意:大小必须是4096字节(4kb)的整数倍,如果你创建4097字节的共享内存,OS 会申请 4096 * 2 个字节。

shmflg:跟 open 函数的打开方式一样,要传宏过去,两个宏:

IPC_CREAT:可以单独传递,宏含义:如果创建的共享内存不存在,就创建它,存在就获取它。潜台词:无论如何都要获取一个共享内存。用来获取共享内存

IPC_EXCL :不能单独使用,其实就是不能把这个宏单独的传过去。

IPC_CREAT | IPC_EXCL :这就是为什么 IPC_EXCL 不能单独传递的原因。宏含义:如果创建的共享内存不存在,就创建它,存在就出错返回。潜台词:只要新的共享内存。创建共享内存。

返回值:获取或创建成功共享内存,则返回共享内存的标识符,类似于身份证号,具有唯一性,反之返回 < 0

问题:OS 怎么知道共享内存存不存在?

答:共享内存是由结构体来描述的,这个结构体有个标识符跟我们的身份证号一样,具有唯一性(就是这个号码只有一个),而且描述共享内存的结构体是由数据结构来组织起来,判断是否有共享内存对于 OS 来说不难。

key 值:这个值是共享内存的身份证,专业术语叫标识符,具有唯一性,这个值只能有我们用户来传递,不能由 OS 创建或传递,因为假设有两个进程,进程 A 创建了共享内存并且 OS 也返回了标识符给该进程,进程 B 要访问到进程 A 创建的共享内存,要拿到进程A创建共享内存的标识符,进程 B 怎么拿到进程 A 创建的共享内存的标识符?,进程之间是有独立性的,而且如果使用其他通信手段拿到这个标识符,代表这个通信方式和其他通信方式混合在一起了,此时就变得更加的复杂,所以这个 key 值只能由我们来传递。

当我们一个共享内存创建成功之后,再次生成共享内存,此时屏幕会提示:

结论:共享内存(包括:system IPC ),他的生命周期随内核;说人话就是:用户不主动删除 ipc 资源,ipc 资源会和你的 OS 一样一直存在,除非你重启系统。

问题:用户怎么主动删除共享内存?

答:1、指令:ipcrm -m shmid值,如:

问题:当共享内存存在时,为什么我们再创建共享内存会创建不了?

答:本质是 key 值冲突了,因为 key 值是用来标识共享内存的,而 shmid 是不冲突的:

key VS shmid

1.key 只在内核中,标识共享内存的唯一性,用户使用共享内存,不用这个 key

2.shmid 只在用户中使用,就是我们写代码时使用这个 shmid 来访问共享内存。

总之:key 相当于文件系统的 inode number ,而 shmid 相当于打开文件的返回值 fd,我们是根据这个文件描述符来对我文件进行读写的,而不是 inode number。

使用代码来删除共享内存的方法:

对于共享内存来说:

1、删除也是控制共享内存的方式之一

2、通过上面这个函数可以获取或者设置共享内存的属性,这也是控制共享内存的方式之一。

上面图片中框出来的就是这个函数的第二个参数的选项:IPC_STAT 可以获取到共享内存的属性,所以我们要给第三个参数设置一个结构体来传递过去。

IPC_RMID:删除共享内存,把第三个参数设置为 nullptr

补充知识:

使用指令:ipcs 这个指令可以查看系统中已创建的共享内存、消息队列等,而 ipcs -m 指令只查看共享内存。

怎么使用共享内存?

我们先把共享内存挂接到进程的虚拟地址空间上:

第二个参数:挂接到进程的虚拟地址空间的那个段,可以由我们用户来指定,但是不推荐我们来指定挂接到虚拟地址空间的哪个段,我们直接设置成 nullptr ,让 OS 帮我们指定。

第三个参数:挂接的方式,跟我们打开文件一样,是以什么方式打开,是读还是写。

返回值:返回挂接成功的进程虚拟地址空间的共享内存的起始地址。挂机失败返回 -1

为什么会挂接失败?

答:我们把权限设置成0了,也就是上面图片的 perms ,没有权限挂接上去没有任何意义,所以挂接失败。而 nattch 表示:起始跟引用计数一样,代表着,有多少个进程挂接了该共享内存,这里的引用计数为 0 ,OS 是不会主动释放共享内存的,因为共享内存的生命周期随 OS。

如果我们创建共享内存时申请4097个字节,此时不是 4096 的整数倍,我们看一下情况:

此时 OS 给我们申请的 4097 个字节,为什么不是 4096 的整数倍呢?

答:如果 OS 给用户 4096 * 2 个字节,那么用户越界访问(访问到第 4098 字节),编译器是不会报错的,此时这个锅就得 OS 背着了,所以 OS 为了不背这个锅,直接就给用户 4097 个字节。

如果进程不行使用共享内存了,此时我们要去挂接,方法:

参数就是去挂接的共享内存在该进程的虚拟地址空间的哪个地方,所以这个参数就是挂接共享内存的虚拟地址的起始地址。

返回值:成功返回0,反之-1

这里要聊下这个函数是可以获取道共享内存的属性的,而图中的 strcut shmid_ds 就是描述共享内存的结构体,这个结构体里面有个 struct ipc_perm 结构体,里面有个变量 _key 保存着我们用户自定义的 key 值

建立通信机制:

共享内存的特点:

1、访问共享内存,不需要系统调用就能访问,因为共享内存已经映射到用户的共享区了。

2、写端把数据写到用户的虚拟地址的共享区,其他进程马上就能看到数据,因为其他进程也映射了共享内存。

3、共享内存是所有进程间通信方式中,速度是最快的。因为管道至少拷贝2次(1次拷贝到管道,另外一次把管道里面的数据拷贝到用户的缓冲区里面),而共享内存就1次(把数据写道进程的虚拟地址的共享区),还有一个原因就是共享内存不需要系统调用(系统调用是需要时间成本的)。

4、缺点:没有资源的保护机制,其实就是没有同步和互斥机制,说人话就是不管共享内存里面有没有数据,进程都可以读。


代码验证:

cpp 复制代码
#pragma once
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <stdio.h>
#include <sys/shm.h>

const int gsize = 128;

//由我们用户来指明
#define PATHNAME "/tmp"
#define PROJ_ID 0x66

class Shm
{
public:
    Shm():_shmid(-1),_size(gsize),_star_addr(nullptr)
    {} 
    ~Shm()
    {}

    //创建共享内存
    void Creat()
    {
        GetHelper(IPC_CREAT | IPC_EXCL | 0666);
    } 

    //获取共享内存
    void Get()
    {
        //既然要获取到共享内存,肯定是获取到已经创建的共享内存
        //所以我们要使用和创建共享内存的那一套方法来生成同一个 key
        GetHelper(IPC_CREAT);
    }

    //去挂接
    void Detach()
    {
        int n = shmdt(_star_addr);
        (void)n;
    }

    //删除共享内存
    void Delete()
    {
        int n = shmctl(_shmid,IPC_RMID,nullptr);
        (void)n;
    }

    //挂接
    void Attach()
    {
        _star_addr = shmat(_shmid,nullptr,0);//创建共享内存时已经设置权限了,不用再设置权限了
        if((long long)_star_addr == -1)
        {
            std::cout << "挂接失败" << std::endl;
            exit(3);
        }
        std::cout << "挂接成功" << std::endl;
    }

    void* Getaddr()
    {
        return _star_addr;
    }

    int GetSize()
    {
        return _size;
    }

    void PrintAttr()//获取共享内存的属性
    {
        struct shmid_ds ds;
        int n = shmctl(_shmid,IPC_STAT,&ds);
        if(n < 0)
        {
            perror("shmctl");
            exit(4);
        }
        printf("key:%d\n",ds.shm_perm.__key);
    }

private:
    key_t Getkey()
    {
        return ftok(PATHNAME,PROJ_ID);
    }

    void GetHelper(int shmflg)
    {
        key_t k = Getkey();
        if (k < 0)
        {
            exit(1); // 生成失误
        }
        _shmid = shmget(k, _size, shmflg); // 0666权限设置:所有进程都可以读写该共享内存
        if (_shmid < 0)
        {
            perror("shmget");
            exit(2);
        }
        printf("k:0x%x,_shmid:%d\n", k, _shmid);
        // 注意:一般来说我们 key 值是可以随便由我们随便写的,但是不推荐,更推荐使用 ftok 函数来生成,这个函数有
        // 两个参数,第一个 _pathname 这个路径必须是真实存在,由我们来随机指定该路径,第二个参数:_proj_id 我们可以随便写一个数字,
        // 这个数字不能为0,至少8个 bite 位,这个函数可以生成一个由这两个参数各取一部分,生成而来,并且返回给我们,如果生成失误,就返回 -1
    }

private:
    int _shmid;
    int _size;//共享内存的大小,单位字节
    void* _star_addr;//挂接成功的虚拟地址的共享内存的起始地址
};
cpp 复制代码
#include "Shm.hpp"


int main()
{
    Shm Serverfile;
    Serverfile.Creat(); 
    Serverfile.Attach();
    sleep(2);
    int size = Serverfile.GetSize();
    char* shm_start = (char*)Serverfile.Getaddr();
    int index = 0;
    //读数据
    while(true)
    {
        for(int i = 0; i < size; i++)
        {
            std::cout << shm_start[i] << " ";
        }
        std::cout << std::endl;
        sleep(1);
    }
    Serverfile.Detach();
    Serverfile.Delete();
    return 0;
}
cpp 复制代码
#include "Shm.hpp"

int main()
{
    Shm Clietnfile;
    Clietnfile.Get();

    Clietnfile.Attach();

    char* shm_start = (char*)Clietnfile.Getaddr();
    int size = Clietnfile.GetSize();
    int index = 0;
    //往共享内存里面写数据
    while(true)
    {
        std::cout << "Please Enter:";
        char ch;
        std::cin >> ch;
        shm_start[index++] = ch;

        index %= size;//防止越界
        sleep(1);
    }
    Clietnfile.Detach();
    //由后端来删除
    return 0;
}
相关推荐
天赐学c语言1 小时前
Linux - 网络基础概念
linux·服务器·网络·socket
oioihoii1 小时前
C++异常安全保证:从理论到实践
开发语言·c++·安全
程序员果子1 小时前
零拷贝:程序性能加速的终极奥秘
linux·运维·nginx·macos·缓存·centos
请叫我7plus2 小时前
用QEMU进行嵌入式Linux开发
linux·驱动开发·嵌入式硬件
啊董dong2 小时前
课后作业-2025年12月07号作业
数据结构·c++·算法·深度优先·noi
兵哥工控2 小时前
MFC PostMessage实现进度条实时更新实例
c++·mfc
天生励志1232 小时前
Nginx安装部署
运维·nginx
李日灐2 小时前
C++STL:list(双链表)的底层实现 && 部分源码解析
开发语言·c++
檀越剑指大厂2 小时前
【Linux系列】Linux中的复制与迁移
linux·运维·服务器