hello~ 很高兴见到大家! 这次带来的是Linux系统中关于进程间通信这部分的一些知识点,如果对你有所帮助的话,可否留下你宝贵的三连呢?
个 人 主 页: 默|笙

在这里插入图片描述



一、共享内存(Shared Memory)

1.1 共享内存原理

在这里插入图片描述

  1. 为实现不同进程访问同一份资源,共享内存的实现思路是:先在物理内存中开辟一块共享内存区域,随后将该区域挂载到需要通信的各进程虚拟地址空间的共享区(挂接),每一个进程都会得到自己这个虚拟内存共享块的起始地址。通过这一方式,不同进程便能访问到同一份内存资源
  2. 这一机制本质上可看作简易版的动态库映射过程,区别在于共享内存无需维护 GOT 表等动态链接相关的表项。共享内存跟它一样就是几个进程共用一个内存块,这样自然能够看到同一份资源。
  3. 根据多个不同进程的通信需求,共享内存是会在内存中开辟多个的,那么操作系统就一定要对这些共享内存进行管理,先描述再组织,也就是要有对应的共享内存结构体。完整共享内存 = 共享内存结构体 + 共享内存数据块。
  4. 使用共享内存的步骤就是:1创建、2关联挂接、3使用、4取消关联、5释放共享内存。
  5. 所以共享内存就是:将同一个物理内存块通过虚拟地址空间映射到不同的进程,从而让这些不同的进程看到同一份资源

1.2 共享内存的使用

创建

在这里插入图片描述

  1. 系统提供了接口shmget函数来创建共享内存。第二个参数是所要创建的共享内存块大小,系统会开辟以 4096 字节为单位的整数倍空间;第三个参数是一个复合参数,用于设置这块共享内存块的访问权限和控制获取内存块的规则。第一个参数叫做键值,是用来唯一标识这个共享内存的(毕竟系统存在很多个共享内存,需要用键值来区分)
  1. 访问权限也就是读写权限比如0666。
  2. 标志位 IPC_EXCL:必须和下面的 IPC_CREAT 一起使用。
  3. 标志位 IPC_CREAT:如果目标共享内存不存在则创建,如果已经存在则返回这个共享内存的编号(获取它)。
  4. 标志位 IPC_EXCL | IPC_CREAT : 如果目标共享内存不存在则创建,如果已经存在则出错返回(返回 - 1)。
  5. 单独一个 IPC_CREAT 用来获取或创建共享内存;而 IPC_EXCL | IPC_CREAT 则用来仅创建全新的共享内存。进程间通信的典型逻辑是:一个进程用 IPC_EXCL | IPC_CREAT 确保创建全新共享内存,另一个进程用单独的 IPC_CREAT 获取这个已存在的共享内存
  1. 如果共享内存创建成功,那么它会返回这个创建成功的共享内存的编号(即共享内存标识符 shmid),如果失败则返回-1。
  2. 键值是用户设置的,为什么要让用户来设置这个用来唯一标识共享内存的键值而不像文件系统那样由系统分配呢?这是因为只能这样做。要实现进程间通信,至少需要两个进程持有同一个键值,才能通过该键值找到对应的共享内存。若键值由系统分配,其他进程将无法获取这个随机分配的键值 —— 毕竟要获取该键值就需要先进行进程间通信,而进行进程间通信又需要先拿到目标键值,这就陷入了 “先有鸡还是先有蛋” 的逻辑悖论。因此,键值只能由用户预先设定,并由用户传递给需要通信的各个进程,以此打破这一悖论
  3. 既然已经有了用于标识共享内存的键值,为何 shmget 还会返回共享内存编号(shmid)?这是因为键值(key)是操作系统层面用于全局唯一标识共享内存的标识,而共享内存编号是用户层操作共享内存的句柄—— 用户对共享内存的后续操作(如挂接、释放等),都需要通过这个编号来完成。这一点可以类比文件系统的设计:共享内存编号对应文件描述符(fd),是用户层操作文件的核心标识;键值则对应 inode 编号,是操作系统内核层面管理文件的标识。

在这里插入图片描述

  1. 不过在实际使用中,我们一般不会手动自定义键值,而是通过 ftok 函数来生成唯一的键值。ftok 函数的核心作用是将 “文件路径 + 一个由用户传递的字符 / 整数” 转换为一个唯一的键值。
// 创建shm
    void Create()
    {
        GetHelper(IPC_CREAT | IPC_EXCL | 0666);
    }
        // 获取shm
    void Get()
    {
        GetHelper(IPC_CREAT);
    }
    key_t GetKey()
    {
        return ftok(PATHNAME, PROJ_ID);
    }
    void GetHelper(int shmflg)
    {
        key_t k = GetKey();
        if (k < 0)
        {
            std::cerr << "GetKey Error" << std::endl;
            exit(GetKey_Error);
        }
        _shmid = shmget(k, _size, shmflg);
        if (_shmid < 0)
        {
            perror("shmget");
            exit(Shmget_Error);
        }
        printf("key:0x%x, _shmid:%d\n", k, _shmid);
    }

查看

在这里插入图片描述

  1. 可以通过命令 ipcs -m来查看系统中已经有的共享内存资源。上面的 nattch 代表有多少进程挂接了这个共享内存。bytes就是大小。

这里为了能够看到现象,大小只申请了128字节,但是OS它真实申请的内存是4096字节,超出 128 字节的这部分空间是操作系统的预留内存,程序无法访问(访问会触发段错误),以此防止内存越界访问。

void PrintAttr()
    {
        struct shmid_ds ds;
        // 将这个共享内存的详细状态信息填充到ds这个结构体变量中
        int n = shmctl(_shmid, IPC_STAT, &ds);
        if (n < 0)
        {
            perror("shmctl");
            exit(Shmctl_Error);
        }
        printf("key: 0x%x\n", ds.shm_perm.__key);
        //打印当前使用这个共享内存的进程数量
        printf("shm_nattch: %ld\n", ds.shm_nattch);
        //打印共享内存的大小
        printf("shm_segsz: 0x%lx\n", ds.shm_segsz);
    }

在这里插入图片描述

  1. shmid_ds 是 Linux 系统 <sys/shm.h> 头文件中定义的共享内存属性结构体,专门用于存储每一块共享内存段的核心元信息(如权限、大小、关联进程数、时间戳等)。这里要注意它的第一个成员,是一个结构体ipc_perm,这个结构体里面存储着这个共享内存的键值等关键信息。
  2. 可以用 shmctl 通过传递 IPC_STAT 来获得该共享内存的详细状态信息。

删除

  1. 可以通过 ipcrm -m + 共享内存编号 命令来删除系统中的共享内存资源。

在这里插入图片描述

  1. 也可以通过系统提供的接口 shmctl 函数来删除。它的第一个参数是要操作的共享内存的编号;第二个参数是具体要执行的命令,比如获取属性、设置属性、删除共享内存(删除就传 IPC_RMID;第三个参数是用来存放或设置共享内存信息的结构体地址,不需要时传 NULL 即可。
  2. 还可以通过传递 IPC_STAT 来获得该共享内存的详细状态信息。
// 删除shm
    void Delete()
    {
        int n = shmctl(_shmid, IPC_RMID, nullptr);
        (void)n;
    }

挂接

在这里插入图片描述

  1. 可以通过系统提供的接口 shmat 函数来完成挂接。它的第一个参数是要求挂载的共享内存的编号;第二个参数是指定共享内存要挂载到的虚拟地址空间的具体地址,实际使用中通常传入 NULL,由操作系统自动分配合适的地址;第三个参数用来设置挂载的权限,告诉OS我这个进程之后想以什么权限来访问这块共享内存,默认传0,一般这个权限在共享内存创建的时候设立。
  2. 如果挂接成功则返回共享内存在当前进程虚拟地址空间的起始地址(这是之后使用共享内存的入口),失败则返回-1
// 挂接
    void Attach()
    {
        _start_addr = shmat(_shmid, nullptr, 0);
        if ((long long)_start_addr == -1)
        {
            perror("shmat");
            exit(Shmat_Error);
        }
    }

去关联

在这里插入图片描述

  1. 可以通过系统提供的接口 shmdt函数来完成去关联。参数传的是要删除的那个共享内存在当前进程虚拟地址空间的起始地址,也就是上面 shmat 函数的返回值。
// 去关联
    void Detach()
    {
        int n = shmdt(_start_addr);
        (void)n;
    }

1.3 共享内存(shm)特点

  1. 共享内存是所有进程间通信(IPC)方式中速度最快的。这是因为进程访问共享内存时,无需调用系统提供的 write 和 read 这类涉及数据拷贝的接口 —— 共享内存已直接映射到进程虚拟地址空间的用户共享区,写端将数据写入共享内存后,其他端口能立即访问到这些数据,无需通过 read 接口读取。对比来看,管道通信一次数据传输至少需要两次内核态 / 用户态的数据拷贝(数据从写进程用户区拷贝到内核缓冲区,再从内核缓冲区拷贝到读进程用户区),而共享内存仅需一次数据拷贝(直接从进程用户区写入共享内存物理区域),因此效率远高于管道等方式。
  2. 但共享内存也存在明显的短板 —— 它本身没有内置的资源保护机制,既不提供互斥能力(防止多个进程同时修改同一数据),也无同步机制(协调进程间的读写顺序)。因此,共享内存的资源保护需要完全由用户自行实现(通常搭配信号量、互斥锁等机制)。
  3. 共享内存(包括整个 IPC 系统下的各类 IPC 资源)的生命周期不随进程,而是与内核生命周期绑定。若用户不主动显式删除这些 IPC 资源,它们会一直驻留在系统中,只有操作系统重启时才会被清理。
  4. 共享内存是全双工通信方式,多个进程可同时对共享内存进行读写操作,数据双向流动。

二、源代码

shm.hpp

#pragma once
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>

#define SIZE 128
#define PATHNAME "/tmp"
#define PROJ_ID 0x22

enum
{
    OK = 0,
    GetKey_Error,
    Shmget_Error,
    Shmat_Error,
    Shmctl_Error
};
class shm
{
public:
    shm() : _shmid(-1), _size(SIZE), _start_addr(nullptr)
    {
    }
    ~shm()
    {
    }
    // 创建shm
    void Create()
    {
        GetHelper(IPC_CREAT | IPC_EXCL | 0666);
    }
    // 删除shm
    void Delete()
    {
        int n = shmctl(_shmid, IPC_RMID, nullptr);
        (void)n;
    }
    // 获取shm
    void Get()
    {
        GetHelper(IPC_CREAT);
    }
    // 挂接
    void Attach()
    {
        _start_addr = shmat(_shmid, nullptr, 0);
        if ((long long)_start_addr == -1)
        {
            perror("shmat");
            exit(Shmat_Error);
        }
    }

    // 去关联
    void Detach()
    {
        int n = shmdt(_start_addr);
        (void)n;
    }

    void PrintAttr()
    {
        struct shmid_ds ds;
        // 将这个共享内存的详细状态信息填充到ds这个结构体变量中
        int n = shmctl(_shmid, IPC_STAT, &ds);
        if (n < 0)
        {
            perror("shmctl");
            exit(Shmctl_Error);
        }
        printf("key: 0x%x\n", ds.shm_perm.__key);
        //打印当前使用这个共享内存的进程数量
        printf("shm_nattch: %ld\n", ds.shm_nattch);
        //打印共享内存的大小
        printf("shm_segsz: 0x%lx\n", ds.shm_segsz);
    }

    void* Addr()
    {
        return _start_addr;
    }

    int Size()
    {
        return _size;
    }

private:
    key_t GetKey()
    {
        return ftok(PATHNAME, PROJ_ID);
    }
    void GetHelper(int shmflg)
    {
        key_t k = GetKey();
        if (k < 0)
        {
            std::cerr << "GetKey Error" << std::endl;
            exit(GetKey_Error);
        }
        _shmid = shmget(k, _size, shmflg);
        if (_shmid < 0)
        {
            perror("shmget");
            exit(Shmget_Error);
        }
        printf("key:0x%x, _shmid:%d\n", k, _shmid);
    }

    int _shmid;
    int _size;
    void *_start_addr;
};

Client.cc

#include <iostream>
#include "shm.hpp"
#include <unistd.h>

int main()
{
    shm sharedmem;
    sharedmem.Get();
    sharedmem.Attach();

    int size = sharedmem.Size();
    char* shm_start = (char*)sharedmem.Addr();
    while (true)
    {
        std::cout << "Please Enter@";
        std::cin >> *shm_start;
        shm_start++;
        sleep(1);
    }
    sharedmem.Detach();
    return 0;
}

Server.cc

#include <iostream>
#include <unistd.h>
#include "shm.hpp"

int main()
{
    shm sharedmem;
    sharedmem.Create();
    sharedmem.Attach();

    sleep(2);
    sharedmem.PrintAttr();

    int size = sharedmem.Size();
    char *shm_start = (char *)sharedmem.Addr();

    while (true)
    {
        for (int i = 0; i < size; i++)
        {
            std::cout << shm_start[i] << " ";
        }
        std::cout << std::endl;
        sleep(1);
    }

    sharedmem.Detach();
    sharedmem.Delete();
    return 0;
}

今天的分享就到此结束啦,如果对读者朋友们有所帮助的话,可否留下宝贵的三连呢~~
让我们共同努力, 一起走下去!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐