欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

Linux 进程间通信 [共享内存

最编程 2024-07-03 22:08:16
...

????前言

共享内存出自 System V 标准,是众多 IPC 解决方案中最快的一种,使用共享内存进行通信时,不需要借助函数进入内核传递数据,而是直接对同一块空间进行数据访问,至于共享内存是如何使用的、通信原理是怎么实现的、以及共享内存+命名管道的组合通信程序该如何实现,都将在本文中解答

天下武功,唯快不破

网络异常,图片无法展示
|


????️正文

1、什么是共享内存?

共享内存 全称 System V 共享内存,是一种进程间通信解决方案,并且是所有解决方案中最快的一个,在通信速度上可以做到一骑绝尘

这是 System V 标准中一个比较成功的通信方式,特点就是非常快,除此之外,System V 标准中还有另外两种通信方式:

  • 消息队列
  • 信号量

这两种通信方式现在已经比较少见了,因为 存在更好的、更实用的通信方式(比如 POSIX 中提供的通信方式)

话不多说,先来看看 System V 共享内存的工作原理:在物理内存中开辟一块公共区域,让两个不同的进程的虚拟地址同时对此空间建立映射关系,此时两个独立的进程能看到同一块空间,可以直接对此空间进行【写入或读取】,这块公共区域就是 共享内存

网络异常,图片无法展示
|

显然,共享内存的目的也是 让不同的进程看到同一份资源

关于共享区:共享区作为虚拟地址空间中一块缓冲区域,既可作为堆栈生长扩展的区域,也可用来存储各种进程间的公共资源,比如这里的共享内存,以及之前学习的动态库,相关信息都是存储在共享区中

注意: 共享内存块的创建、进程间建立映射都是由 OS 实际执行的


2、共享内存的相关知识

在正式使用共享内存通信之前,需要先学习一下 共享内存的相关知识,因为这里的共享内存出自 System V 标准,所以 System V 中的消息队列、信号量绝大部分接口的风格也与之差不多

2.1、共享内存的数据结构

共享内存不止用于两个进程间通信,所以共享内存必须确保能持续存在,这也就意味着共享内存的生命周期不随进程,而是随操作系统,一旦共享内存被创建,除非被删除,否则将会一直存在,因此 操作系统需要对共享内存的状态加以描述

共享内存也不止存在一份,当出现多块共享内存时,操作系统不可能一一比对进行使用,秉持着高效的原则,操作系统会把已经创建的共享内存组织起来,更好的进行管理

所以共享内存需要有自己的数据结构,经过操作系统 先描述,再组织 后,构成了下面这个数据结构

注:shm 表示共享内存

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 */
};


其中 struct ipc_perm 中存储了 共享内存中的基本信息,具体包含内容如下:

struct ipc_perm
{
    __kernel_key_t key; 
    __kernel_uid_t uid;
    __kernel_gid_t gid;
    __kernel_uid_t cuid;
    __kernel_gid_t cgid;
    __kernel_mode_t mode; 
    unsigned short seq;
};


共享内存虽然属于文件系统,但它的结构是经过特殊设计的,与文件系统中的 inode 那一套结构逻辑不一样


2.2、创建 shmget

创建共享内存时,需要借助 shmget 这个函数

网络异常,图片无法展示
|


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


关于 shmget 函数

组成部分 含义
返回值 int 创建成功返回共享内存的 shmid,失败返回 -1
参数1 key_t key 创建共享内存时的唯一 key 值,通过函数计算获取
参数2 size_t size 创建共享内存的大小,一般为 4096
参数3 int shmflg 位图,可以设置共享内存的创建方式及创建权限

因为共享内存拥有自己的数据结构,所以 返回值 int 实际就是 shmid,类似于文件系统中的 fd,用来对不同的共享内存块进行操作

参数2为创建共享内存的大小,单位是字节,一般设为 4096 字节(4kb),与一个 PAGE 页大小相同,有利于提高 IO 效率

参数3是位图结构,类似于 open 函数中的参数3(文件打开方式),常用的选项有以下几个:

  1. IPC_CREAT 创建共享内存,如果存在,则使用已经存在的
  2. IPC_EXCL 避免使用已存在的共享内存,不能单独使用,需要配合 IPC_CREAT 使用,作用是当创建共享内存时,如果共享内存已经存在,则创建失败
  3. 权限 因为共享内存也是文件,所以权限可设为文件的起始权限 0666

而参数1比较特殊,key_t 实际就是对 int 进行了封装,表示一个数字,用来标识不同的共享内存块,可以理解为 inode,因为是标识值,所以必须确保 唯一性,需要使用函数 ftok 根据不同的 项目路径 + 项目编号 + 特殊的算法,生成一个碰撞率低的标识值,供操作系统对共享内存进行区分和调用

2.2.1、关于 key 的获取

使用函数 ftok 生成 key

网络异常,图片无法展示
|

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


关于 ftok 函数

组成部分 含义
返回值 key_t 返回生成的标识值,等价于 int 类型
参数1 const char *pathname 项目路径,可使用 绝对 或 相对 路径
参数2 int proj_id 项目编号,可以根据实际情况编写

注意: 只有先让操作系统根据同一个 key 创建/打开 同一个共享内存,不同的进程才能看到同一份资源

下面是创建 共享内存 的代码

common.h

#include <iostream>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
using namespace std;
#define PATHNAME "." // 项目名
#define PROJID 0x29C         // 项目编号
const int gsize = 4096;
const mode_t mode = 0666;
//将十进制数转为十六进制数
string toHEX(int x)
{
    char buffer[64];
    snprintf(buffer, sizeof buffer, "0x%x", x);
    return buffer;
}
// 获取key
key_t getKey()
{
    key_t key = ftok(PATHNAME, PROJID);
    if (key == -1)
    {
        // 失败,终止进程
        cerr << "ftok fail!  "
             << "errno: " << errno << " | " << strerror(errno) << endl;
        exit(1);
    }
    return key;
}
// 共享内存助手
int shmHelper(key_t key, size_t size, int flags)
{
    int shmid = shmget(key, size, flags);
    if (shmid == -1)
    {
        // 失败,终止进程
        cerr << "shmget fail!  "
             << "errno: " << errno << " | " << strerror(errno) << endl;
        exit(2);
    }
    return shmid;
}
// 创建共享内存
int createShm(key_t key, size_t size)
{
    return shmHelper(key, size, IPC_CREAT | IPC_EXCL | mode);
}
// 获取共享内存
int getShm(key_t key, size_t size)
{
    return shmHelper(key, size, IPC_CREAT);
}


server.cc

#include <iostream>
#include "common.h"
using namespace std;
int main()
{
    // 服务端创建共享内存
    key_t key = getKey();
    int shmid = createShm(key, gsize);
    cout << "server key: " << toHEX(key) << endl;
    cout << "server shmid: " << shmid << endl;
    return 0;
}


client.cc

#include <iostream>
#include "common.h"
using namespace std;
int main()
{
    // 客户端打开共享内存
    key_t key = getKey();
    int shmid = getShm(key, gsize);
    cout << "client key: " << toHEX(key) << endl;
    cout << "client shmid: " << shmid << endl;
    return 0;
}


运行结果如下:


通过 shmgetftok 函数获得唯一的 keyshmid

创建出来的共享内存可以通过 ipcs -m 查看

ipcs -m



共享内存 301465 就是通过上述代码生成的

注意:因为共享内存每次都是随机生成的,所以每次生成的 keyshmid 都不一样

2.3、释放共享内存

当我们再次运行程序时,会出现下面这种情况:


服务端运行失败,原因是 shmget 创建共享内存失败,这是因为服务端创建共享内存时,传递的参数为 IPC_CREAT | IPC_EXCL,其中 IPC_EXCL 注定了当共享内存存在时,创建失败

而客户端只是单纯的获取共享内存,同时也只传递了 IPC_CREAT 参数,所以运行才会成功

综上所述,服务端运行失败的根本原因是 待创建的共享内存已存在,如果想要成功运行,需要先将原共享内存释放

共享内存的释放方式主要有以下两种:

2.3.1、通过指令释放

可以直接在命令行中通过指令,根据 shmid 释放指定共享内存