Linux进程间通信【共享内存】

news2024/11/24 13:48:23

✨个人主页: 北 海
🎉所属专栏: Linux学习之旅
🎃操作环境: CentOS 7.6 阿里云远程服务器

成就一亿技术人


文章目录

  • 🌇前言
  • 🏙️正文
    • 1、什么是共享内存?
    • 2、共享内存的相关知识
      • 2.1、共享内存的数据结构
      • 2.2、创建 shmget
        • 2.2.1、关于 key 的获取
      • 2.3、释放共享内存
        • 2.3.1、通过指令释放
        • 2.3.2、通过共享内存控制函数释放
      • 2.4、进程关联 shmat
      • 2.5、进程去关联 shmdt
      • 2.6、共享内存控制 shmctl
    • 3、共享内存简单使用
    • 4、共享内存的补充知识
      • 4.1、共享内存的大小
      • 4.2、共享内存 “快” 的原因
      • 4.3、共享内存的缺点
    • 5、共享内存实操--配合命名管道完成通信
      • 5.1、逻辑设计
      • 5.3、效果演示
      • 5.4、注意事项
      • 5.5、完整源码
  • 🌆总结


🌇前言

共享内存出自 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 释放指定共享内存

ipcrm -m shmid

释放

共享内存已被释放

2.3.2、通过共享内存控制函数释放

这里先提前使用一下函数 shmctl,在服务端中加入删除共享内存的函数,当服务端运行结束时,自动删除共享内存

shmctl(shmid, IPC_RMID, NULL);

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;

    int n = 5;
    while(n)
    {
        //运行五秒后删除共享内存
        cout << n-- << endl;
        sleep(1);
    }

    shmctl(shmid, IPC_RMID, NULL);
    
    return 0;
}

结果
共享内存也被成功释放了,实际中会使用函数进行自动释放,因为手动释放比较麻烦

2.4、进程关联 shmat

共享内存在被成功创建后,进程还不 “认识” 它,只有让待通信进程都 “认识” 同一个共享内存后,才能进行正常通信,让进程 “认识” 共享内存这一操作称为 关联

当进程与共享内存关联后,共享内存才会 通过页表映射至进程的虚拟地址空间中的共享区中

需要使用 shmat 函数进行关联

图示

#include <sys/types.h>
#include <sys/shm.h>

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

关于 shmat 函数

组成部分含义
返回值 void*如同 malloc 一样,返回的是 void* 指针,可以根据需求进行强转
参数1 int shmid待关联的共享内存 id
参数2 const void *shmaddr共享内存关联至进程共享区的地址,可以不用管
参数3 int shmflg关联后,进程对共享内存的读写属性

当进程与共享内存关联后,返回的就是共享内存映射至共享区的起始地址

  • 关联成功返回起始地址
  • 关联失败返回 (void*) -1

共享内存映射至共享区时,我们可以指定映射位置(即传递参数2),但我们一般不知道具体地址,所以 可以传递 NULL,让编译器自动选择位置进行映射

关于参数3,一般直接设为 0,表示关联后,共享内存属性为 默认读写权限,更多选项如下所示:

  • SHM_RDONLY 关联共享内存后只进行读取操作
  • SHM_RNDshmaddr 不为 NULL,则关联地址自动向下调整为 SHMLBA 的整数倍,SHMLBA 的值为 PAGE_SIZE,具体调整公式:shmaddr - (shmaddr % SHMLBA)

一般通信数据为字符,所以可以将 shmat 的返回值强转为 char*

下面是 服务端 和 客户端 关联共享内存 的代码

服务端睡五秒后结束,客户端睡三秒后就结束,监视窗口每隔2秒更新一次

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;

    char *start = (char*)shmat(shmid, NULL, 0);
    if ((void*)start == (void*)-1)
    {
        cerr << "shmat fail!"
             << "errno: " << errno << " | " << strerror(errno) << endl;
        shmctl(shmid, IPC_RMID, NULL);  //即使异常了,也要把共享内存释放
        exit(1);
    }

    //挂接成功后,睡五秒再释放
    printf("start: %p\n", start);
    sleep(5);

    shmctl(shmid, IPC_RMID, NULL);

    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;

    char *start = (char *)shmat(shmid, NULL, 0);
    if ((void *)start == (void *)-1)
    {
        cerr << "shmat fail!"
             << "errno: " << errno << " | " << strerror(errno) << endl;
        exit(1);
    }

    // 挂接成功后,睡三秒就结束
    printf("start: %p\n", start);
    sleep(3);

    return 0;
}

图示
共享内存信息中的 nattch 表示当前共享内存中的进程关联数

注意: 程序运行结束后,会自动取消关联状态

2.5、进程去关联 shmdt

可以手动去关联,即使用函数 shmdt

图示

 #include <sys/types.h>
 #include <sys/shm.h>

 int shmdt(const void *shmaddr);

这个函数使用非常简单,将已关联的共享内存地址传递进行去关联即可

返回值:去关联成功返回 0,失败返回 -1,并将错误码设置

如同关闭 FILE*fdfree 等一些列操作一样,当我们关联共享内存,使用结束后,需要进行去关联,否则会造成内存泄漏(指针指向共享内存,访问数据)

所以需要在上面的代码结尾加上 shmdt(start) 去关联

注意:

  • 共享内存在被删除后,已成功挂接的进程仍然可以进行正常通信,不过此时无法再挂接其他进程

  • 共享内存被提前删除后,状态 status 变为 销毁 dest

2.6、共享内存控制 shmctl

System V 标准中还为共享内存提供了一个控制函数 shmctl,其原型如下图所示:

图示
关于 shmctl 函数

组成部分含义
返回值 int成功返回 0,失败返回 -1
参数1 int shmid待控制的共享内存 id
参数2 int cmd控制共享内存的具体动作,同样是位图
参数3 struct shmid_ds *buf用于获取或设置所控制共享内存的数据结构

之前在释放共享内存时,我们就已经使用过了 shmctl,给参数2传入的是 IPC_RMID,表示删除共享内存,除此之外,还可以给参数2传递以下动作:

  • IPC_STAT 用于获取或设置所控制共享内存的数据结构
  • IPC_SET 在进程有足够权限的前提下,将共享内存的当前关联值设置为 buf 数据结构中的值

buf 就是共享内存的数据结构,可以使用 IPC_STAT 获取,也可以使用 IPC_SET 设置

当参数2为 IPC_RMID 时,参数3可以不用传递;其他两种情况都需传递 struct shmid_ds *buf

演示代码:通过 shmctl 获取共享内存的数据结构,并从中获取 pidkey

#include <iostream>
#include "common.h"

using namespace std;

int main()
{
    // 服务端创建共享内存
    key_t key = getKey();
    int shmid = createShm(key, gsize);

    cout << "getpid(): " << getpid() << endl;
    cout << "server key: " << toHEX(key) << endl;

    char *start = (char*)shmat(shmid, NULL, 0); //去关联
    if ((void*)start == (void*)-1)
    {
        cerr << "shmat fail!"
             << "errno: " << errno << " | " << strerror(errno) << endl;
        shmctl(shmid, IPC_RMID, NULL);  //即使异常了,也要把共享内存释放
        exit(1);
    }

    struct shmid_ds buf;
    int n = shmctl(shmid, IPC_STAT, &buf);
    if (n == -1)
    {
        cerr << "shmctl fail!"
             << "errno: " << errno << " | " << strerror(errno) << endl;
        shmctl(shmid, IPC_RMID, NULL);  //即使异常了,也要把共享内存释放
        exit(1);
    }

    cout << "==================" << endl;
    cout << "buf.shm_cpid: " << buf.shm_cpid << endl;
    cout << "buf.shm_perm.__key: " << toHEX(buf.shm_perm.__key) << endl;


    shmdt(start);   //去关联
    shmctl(shmid, IPC_RMID, NULL);
    return 0;
}

结果

通过程序证明了 共享内存确实有自己的数据结构

结论: 共享内存 = 共享内存的内核数据结构(struct shmid_ds) + 真正开辟的空间


3、共享内存简单使用

当两个进程与同一块共享内存成功关联后,可以直接对该区域进行读写操作,就像 父子进程读取同一个数据一样,不过不能进行写入,因为会发生 写时拷贝 机制,拷贝共享数据

但共享内存就不一样了,真·共享,不会发生 写时拷贝

简单使用共享内存流程如下:

  • 创建、关联共享内存
  • 客户端向服务端写入数据(字符串)
  • 服务端每隔十秒读取一次

为了使操作更加简洁,可以将 common.h 中的代码封装为一个类,创建、关联、去关联等操作一气呵成

common.hpp

#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 // 项目编号

enum
{
    SERVER = 0,
    CLIENT = 1
};

class shm
{
public:
    shm(int id)
        : _id(id)
    {
        _key = getKey();    //获取 key

        // 根据不同的身份,创建 / 打开 共享内存
        if (_id == SERVER)
            _shmid = shmHelper(_key, gsize, IPC_CREAT | IPC_EXCL | mode);
        else
            _shmid = shmHelper(_key, gsize, IPC_CREAT);

        // 关联共享内存
        _start = shmat(_shmid, NULL, 0); // 关联
        if (_start == (void *)-1)
        {
            cerr << "shmat fail!"
                 << "errno: " << errno << " | " << strerror(errno) << endl;
            exit(1);
        }
    }

    ~shm()
    {
        // 去关联
        int n = shmdt(_start);
        if (n == -1)
        {
            cerr << "shmdt fail!"
                 << "errno: " << errno << " | " << strerror(errno) << endl;
            exit(1);
        }

        // 根据不同的身份,判断是否需要删除共享内存
        if(_id == SERVER)
            shmctl(_shmid, IPC_RMID, NULL);
    }

    key_t getKey() const
    {
        key_t key = ftok(PATHNAME, PROJID);
        if (key == -1)
        {
            // 失败,终止进程
            cerr << "ftok fail!  "
                 << "errno: " << errno << " | " << strerror(errno) << endl;
            exit(1);
        }

        return key;
    }

    int getShmID() const
    {
        return _shmid;
    }

    void *getStart() const
    {
        return _start;
    }

protected:
    static const int gsize = 4096;
    static const mode_t mode = 0666;

    // 将十进制数转为十六进制数
    string toHEX(int x)
    {
        char buffer[64];
        snprintf(buffer, sizeof buffer, "0x%x", x);
        return buffer;
    }

    // 共享内存助手
    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;
    }

private:
    key_t _key;
    int _shmid = 0;
    void *_start;
    int _id; // 身份标识符,用来区分服务端与客户端
};

server.cc

#include <iostream>
#include <cstring>
#include <unistd.h>
#include "common.hpp"

using namespace std;

int main()
{
    // 服务端
    shm s(SERVER);

    // 获取共享内存起始地址
    char *start = (char *)s.getStart();

    // 开始通信
    while (true)
    {
        cout << "server get: " << start << endl;

        //读取到26个字母后,关闭服务端
        if (strlen(start) == 26)
            break;

        sleep(1);
    }

    return 0;
}

client.cc

#include <iostream>
#include "common.hpp"

using namespace std;

int main()
{
    // 客户端
    shm c(CLIENT);

    // 获取共享内存起始地址
    char *start = (char *)c.getStart();

    // 开始通信
    int n = 0;
    printf("client sent: ");
    fflush(stdout); //细节:强制刷新缓冲区

    //写入26个字母后,终止客户端
    while (n < 26)
    {
        start[n] = ('A' + n);
        printf("%c", start[n]);
        fflush(stdout); //细节:强制刷新缓冲区

        n++;
        start[n] = '\0';
        sleep(1);
    }
    cout << endl;

    return 0;
}

运行效果如下:

图示

注意:

  • 如果想实现 client 不按回车打印数据,需要使用 fflush 手动刷新 printf 的缓冲区
  • 需要先启动服务端,才启动客户端;如果先启动了客户端,会导致客户端创建共享内存后,无法释放,程序也无法运行
  • 因为共享内存不区分读端与写端,只要关联了,两者都可以进行读写

4、共享内存的补充知识

关于共享内存,还需要知道以下几个特点

4.1、共享内存的大小

在上面的代码中,我们将共享内存的大小设为 4096 字节,即一个 PAGE 页的大小(4kb);如果申请 4097 字节大小的共享内存,操作系统实际上会分配 8192 字节(8kb 的空间),但供共享内存使用的只有 4097 字节

为什么会出现这种现象?

  • 因为操作系统为了避免因非法操作导致出现越界访问问题,所以会开辟 PAGE 页的整数倍大小空间,多开辟的空间不会给共享内存时,主要是用来检测是否出现了越界访问

图示

4.2、共享内存 “快” 的原因

共享内存 IPC 快的秘籍在于 减少数据拷贝(IOIO 是很慢、很影响效率的

比如在使用管道通信时,需要经过以下几个步骤:

  1. 从进程 A 中读取数据(IO
  2. 打开管道,然后通过系统调用将数据写入管道(IO
  3. 通过系统调用从管道读取数据(IO
  4. 将读取到的数据输出至进程 BIO

也就说,使用管道通信至少需要经过 4IO

管道通信

但共享内存就不一样,直接访问同一块区域进行数据读写

在使用共享内存通信时,只需要经过以下两步:

  1. 进程 A 直接将数据写入共享内存中
  2. 进程 B 直接从共享内存中读取数据

显然,使用共享内存只需要经过 2IO

图示

所以共享内存的秘籍是 减少拷贝(IO)次数

  • 得益于共享内存的这种特性,可以让进程通信的时候,减少拷贝次数,所以共享内存是所有进程通信中,速度最快的

4.3、共享内存的缺点

共享内存这么快,为什么不直接只使用共享内存呢?

因为快是要付出代价的,因为 “快” 导致共享内存有以下缺点:

  • 多个进程无限制地访问同一块内存区域,导致共享内存中的数据无法确保安全
  • 即 共享内存 没有同步和互斥机制,某个进程可能数据还没写完,就被别人读走了,或者被别人覆盖了

总的来说,不加规则限制的共享内存是不推荐使用的

就像 《唐伯虎点秋香》 中船夫一样
(船上)
唐伯虎: 哎,兄弟啊,给我追一下华府的官船。
船夫: 好!公子,小心小心啊!
船夫: 公子,你还真识货,这么多船,你偏偏挑中了我这条船,我可是出了名的快啊。
唐伯虎: 是吗?
船夫: 当然了。
唐伯虎: 哎~~~你的船在下沉哎!
船夫: 我不是说了,沉也沉得快嘛。

图示

当然可以利用其他通信方式,控制共享内存的写入与读取规则

  • 比如使用命名管道,进程 A 写完数据后,才通知进程 B 读取
  • 进程 B 读取后,才通知进程 A 写入

假如是多端写入、多端读取的场景,则 可以引入生产者消费者模型,加入互斥锁和条件变量等待工具,控制内存块的读写


5、共享内存实操–配合命名管道完成通信

共享内存如果不加以控制的话,很难实现管道般的通信,所以我们要对它进行改造

5.1、逻辑设计

共享内存的特点是 无读写规则限制,进程即可读也可写,容易造成冲突,因此我们可以对其加以限制,所使用的工具正是上文中学习的 命名管道

场景:两个独立进程使用共享内存实现通信

所需要资源:一块共享内存,两条命名管道

  • 一条管道负责 服务端写,客户端读,另一条管道则负责 服务端读,客户端写,间接实现 双向通知

可能有的人想问:为什么不直接使用共享内存通知?答案很简单,我们加入命名管道的目的就是为了实现进程间使用共享内存通信,当然不能使用 共享内存 -> 辅助实现共享内存通信,这不合理

图示

所以我们这个程序的逻辑设计流程如下:

  1. 创建共享内存,将服务端、客户端进程关联
  2. 创建两条管道,分别让服务端、客户端以不同方式打开
  3. 进行通信

因为大部分操作之前都已经学过了,所以这里直接先演示效果,然后说明一下注意事项,想提前看看源码的同学可以跳转至最后一个部分

5.3、效果演示

这里模拟实现的是 客户端写,服务端读,如果想反转,更改读写逻辑即可,因为共享内存支持双向通信

图示

5.4、注意事项

在这份代码中,我们需要注意 谁先启动的问题,因为是两条命名管道,刚开始都在等对方写入数据,所以必须由一方先出击,打破这种 无限等待 的破局,建议谁读取,谁就先通知,即在执行通信代码前,通知 写入方 可以写入数据了

关于其他值得 注意 的点:

  • 打开命名管道文件时,需要特别注意,别打开错了
  • 在通信结束后,需要删除命名管道文件

5.5、完整源码

将 共享内存 和 命名管道 的前置准备工作进行封装,代码极其优雅

common.h

#include <iostream>
#include <cerrno>
#include <cassert>
#include <cstring>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

using namespace std;

#define PATHNAME "." // 项目名
#define PROJID 0x29C // 项目编号

// 两条管道名
const char *fifo_name1 = "fifo1";
const char *fifo_name2 = "fifo2";

enum
{
    SERVER = 0,
    CLIENT = 1
};

class shm
{
public:
    shm(int id)
        : _id(id)
    {
        _key = getKey(); // 获取 key

        // 根据不同的身份:
        //    创建 / 打开 共享内存
        //    创建 / 打开 命名管道
        if (_id == SERVER)
        {
            _shmid = shmHelper(_key, gsize, IPC_CREAT | IPC_EXCL | mode);

            int n = mkfifo(fifo_name1, mode);
            assert(n != -1);
            (void)n;

            n = mkfifo(fifo_name2, mode);
            assert(n != -1);
            (void)n;

            // 服务端以写打开命名管道1,以读打开命名管道2
            _wfd = open(fifo_name1, O_WRONLY);
            _rfd = open(fifo_name2, O_RDONLY);
        }
        else
        {
            _shmid = shmHelper(_key, gsize, IPC_CREAT);

            // 客户端以读打开命名管道1,以写打开命名管道2
            _rfd = open(fifo_name1, O_RDONLY);
            _wfd = open(fifo_name2, O_WRONLY);
        }

        // 关联共享内存
        _start = shmat(_shmid, NULL, 0); // 关联
        if (_start == (void *)-1)
        {
            cerr << "shmat fail!"
                 << "errno: " << errno << " | " << strerror(errno) << endl;
            exit(1);
        }
    }

    ~shm()
    {
        // 关闭fd
        close(_wfd);
        close(_rfd);

        // 去关联
        int n = shmdt(_start);
        if (n == -1)
        {
            cerr << "shmdt fail!"
                 << "errno: " << errno << " | " << strerror(errno) << endl;
            exit(1);
        }

        // 根据不同的身份:
        //    判断是否需要删除管道文件
        //    判断是否需要删除共享内存
        if (_id == SERVER)
        {
            unlink(fifo_name1);
            unlink(fifo_name2);
            shmctl(_shmid, IPC_RMID, NULL);
        }
    }

    key_t getKey() const
    {
        key_t key = ftok(PATHNAME, PROJID);
        if (key == -1)
        {
            // 失败,终止进程
            cerr << "ftok fail!  "
                 << "errno: " << errno << " | " << strerror(errno) << endl;
            exit(1);
        }

        return key;
    }

    int getShmID() const
    {
        return _shmid;
    }

    void *getStart() const
    {
        return _start;
    }

    int getWFD() const
    {
        return _wfd;
    }

    int getRFD() const
    {
        return _rfd;
    }

protected:
    static const int gsize = 4096;
    static const mode_t mode = 0666;

    // 共享内存助手
    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;
    }

private:
    key_t _key;
    int _shmid = 0;
    void *_start;
    int _wfd; // 写端 与 读端 fd
    int _rfd;
    int _id; // 身份标识符,用来区分服务端与客户端
};

server.cc

#include <iostream>
#include <cstring>
#include <unistd.h>

#include "common.hpp"

using namespace std;

int main()
{
    // 服务端:读取
    shm s(SERVER);

    char *start = (char *)s.getStart();
    int wfd = s.getWFD();
    int rfd = s.getRFD();

    const char *str = "yes";

    // 因为是服务端先启动,所以直接先向管道中发出 yes 的指令
    write(wfd, str, strlen(str));

    char buff[64];
    while (true)
    {
    	// 等待客户端发出操作命令
        int n = read(rfd, buff, sizeof(buff) - 1);
        buff[n] = 0;

        if (n > 0)
        {
            if (strcasecmp(str, buff) == 0)
            {
                // 客户端允许服务端进行读取
                int i = 0;
                while (start[i])
                {
                    buff[i] = start[i];
                    i++;
                }
                buff[i] = 0;

                printf("server read# %s\n", buff);

                if (strcasecmp("exit", buff) == 0)
                    break;

                // 读取完成,通知客户端写入
                write(wfd, str, strlen(str));
            }
        }
        else if (n == 0)
        {
            cerr << "客户端未从管道中读取到数据" << endl;
        }
        else
        {
            cerr << "读取异常" << endl;
            break;
        }
    }

    return 0;
}

client.cc

#include <iostream>
#include <cstring>
#include <unistd.h>

#include "common.hpp"

using namespace std;

int main()
{
    // 客户端:写入
    shm c(CLIENT);

    char *start = (char *)c.getStart();
    int wfd = c.getWFD();
    int rfd = c.getRFD();

    const char *str = "yes";

    srand((size_t)time(NULL));

    char buff[64];
    while (true)
    {
    	// 等待服务端发出操作命令
        int n = read(rfd, buff, sizeof(buff) - 1);
        buff[n] = 0;

        if (n > 0)
        {
            if (strcasecmp(str, buff) == 0)
            {
                printf("client write# ");
                fflush(stdout);

                fgets(buff, sizeof buff, stdin);
                int i = 0;
                while(buff[i] != '\n')
                {
                    start[i] = buff[i];
                    i++;
                }
                buff[i] = start[i] = 0;


                // 写入完成,通知服务端读取
                write(wfd, str, strlen(str));

                if (strcasecmp("exit", buff) == 0)
                    break;
            }
        }
        else if (n == 0)
        {
            cerr << "客户端未从管道中读取到数据" << endl;
        }
        else
        {
            cerr << "读取异常" << endl;
            break;
        }
    }

    return 0;
}

本文中涉及的所有代码均在此仓库中:《共享内存博客仓库》


🌆总结

以上就是本次关于 Linux 进程间通信之 共享内存 的全部内容了,共享内存 是所有 IPC 中最快的一种,因为它省去了很多不必要的 IO 操作,进程直接对话进程,效率极高,不过在狂飙的后果就是不安全,因此在实现 共享内存 实现进程间通信时,需要借助其他 IPC 方式控制共享内存,这样才能合理发挥 共享内存 的实力


星辰大海

相关文章推荐

Linux进程间通信【命名管道】

Linux进程间通信【匿名管道】

Linux基础IO【软硬链接与动静态库】

Linux基础IO【深入理解文件系统】

Linux【模拟实现C语言文件流】

Linux基础IO【重定向及缓冲区理解】

Linux基础IO【文件理解与操作】

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/628138.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

人工智能十大新星揭晓,华人学者占90%

人工智能领域著名杂志 IEEE Intelligent Systems发布了 2022 年度“人工智能十大新星”&#xff08;AIs 10 to Watch&#xff09;名单 &#xff0c;其中有九位都是华人研究者。知识人网小编推荐给大家。 近日&#xff0c;人工智能领域著名杂志 IEEE Intelligent Systems公布了 …

在JavaScript中的栈数据结构(Stack )

文章目录 导文什么是Stack 类&#xff1f;如何创建一个Stack如何修改Stack中的值栈声明方法举例添加移除查看查看栈顶元素检查栈是否为空检查栈的长度 清空栈元素打印栈元素 完整的Stack函数&#xff1a;创建Stack的其他方法-用 ES6 语法声明 Stack 类 使用Stack类在 JavaScrip…

关于GDPR体系文件介绍,介绍GDPR体系文件的内容和意义

随着数字化时代的到来&#xff0c;个人数据保护成为了一个日益受到关注的问题。欧盟于2018年5月25日颁布了“通用数据保护条例”&#xff08;GDPR&#xff09;&#xff0c;旨在加强对欧洲公民个人数据的保护。GDPR对企业和组织的数据保护和处理流程提出了严格的要求&#xff0c…

自助化打印面单教程

我们都知道&#xff0c;这几年快递行业&#xff0c;从传统纸质面单过渡到了电子面单。以往企业寄快递&#xff0c;能够自行填写纸质面单&#xff0c;等待收件员上门收件&#xff0c;现如今&#xff0c;企业寄件能否自行打印电子面单&#xff1f; 首先我们要先对比一下传统面单和…

云智研发笔试编程题(一):图像相似度

题目描述 给出两幅相同大小的黑白图像 (用0-1矩阵) 表示求它们的相似度。若两幅图像在相同位置上的像素点颜色相同&#xff0c;则称它们在该位置具有相同的像素点。两幅图像的相似度定义为相同像素点数占总像素点数的百分比。 输入描述 第一行包含两个整数m和n&#xff0c;表…

C++概述——浅谈C++对C的拓展

纵有疾风起&#xff0c;人生不言弃。本文篇幅较长&#xff0c;如有错误请不吝赐教&#xff0c;感谢支持。 &#x1f4ac;C核心编程一 一.C简介二.第一个程序Hello,world&#xff01;三.C的特点四.C对C的扩展1️⃣作用域运算符::2️⃣C命名空间(namespace)①名字控制②为什么有…

golang性能分析 pprof的使用 graphviz

golang性能分析 pprof的使用 graphviz 1 参考文档2 pprof、Graphviz介绍3 Graphviz下载 安装4 使用 1 参考文档 参考1&#xff1a;golang性能分析&#xff0c;pprof的使用&#xff0c;graphviz&#xff0c;火焰图 参考2&#xff1a;Golang中的pprof分析环境搭建【Windows环境】…

隆重共建开放,共享未来 | 2023 开放原子全球开源峰会 OpenAtom OpenHarmony 分论坛即将启幕

在全球数字化进程快速发展的背景下&#xff0c;OpenAtom OpenHarmony&#xff08;以下简称“OpenHarmony”&#xff09;旨在通过面向全场景、全连接、全智能时代、基于开源的方式&#xff0c;搭建下一代智能终端设备操作系统的框架和平台&#xff0c;为消费、金融、能源、教育、…

光模块失效的原因及预防措施汇总

光模块从生产到使用都必须有规范化的操作方法&#xff0c;任何不规范的动作都可能造成光模块隐性的损伤或者永久的失效&#xff0c;下面就跟着小编来学习一下预防光模块失效的方法吧! 一、光模块失效的主要原因 1、光模块受到静电伤害&#xff08;ESD损伤&#xff09;&#xf…

2023 年最新版 Java 后端最全面试攻略,全面对标 BATJ互联网大厂

前言 小编分享的这份 Java 后端开发面试总结包含了 JavaOOP、Java 集合容器、Java 异常、并发编程、Java 反射、Java 序列化、JVM、Redis、Spring MVC、MyBatis、MySQL 数据库、消息中间件 MQ、Dubbo、Linux、ZooKeeper、 分布式 &数据结构与算法等 25 个专题技术点&#…

爬虫案例-使用Session登录某知名网站(JS逆向AES-CBC加密+MD5加密)

总体概览&#xff1a;使用Session登录该网站&#xff0c;其中包括对password参数进行js逆向破解 &#xff08;涉及加密&#xff1a;md5加密AES-CBC加密&#xff09; 难度&#xff1a;两颗星 目标网址&#xff1a;aHR0cHM6Ly93d3cuZnhiYW9nYW8uY29tLw 下面文章将分为四个部分…

证券公司软件测试面试总结分享!

这家公司是做证券项目的&#xff0c;约的9点钟&#xff0c;路程还是有点遥远&#xff0c;转了一趟公交两趟地铁&#xff0c;精力都花在了路上&#xff0c;感觉有点累&#xff0c;以下是今天得面试流程。 到公司前台给我了一张面试表&#xff0c;写完之后就是等待面试。一共面试…

Java开发手册中为什么禁止使用isSuccess作为布尔类型变量名以及POJO中基本类型与包装类型的使用标准

场景 Java开发手册中关于POJO的布尔类型的变量名的要求是: 【强制】POJO 类中的任何布尔类型的变量&#xff0c;都不要加 is 前缀&#xff0c;否则部分框架解析会引起序列化错误。 说明&#xff1a;在本文 MySQL 规约中的建表约定第一条&#xff0c;表达是与否的变量采用 is…

【新版】系统架构设计师 - 知识产权与标准化

个人总结&#xff0c;仅供参考&#xff0c;欢迎加好友一起讨论 文章目录 架构 - 知识产权与标准化考点摘要保护范围与对象识产权与保护期限知识产权与知识产权人的确定知识产权与侵权判定标准化&#xff08;标准分类与编号&#xff09; 架构 - 知识产权与标准化 考点摘要 保护…

JavaSE-06 【面向对象+封装】

JavaSE-06 [面向对象封装] 第一章 面向对象思想 1.1 面向过程和面向对象 面向过程&#xff1a; 面向过程就是分析出解决问题所需要的步骤&#xff0c;然后用函数把这些步骤一步一步实现&#xff0c;使用的时候一个一个依次调用就可以了面向对象&#xff1a; 面向对象是把构成…

windows服务器自带IIS搭建网站并发布公网访问的详细教程

文章目录 1.前言2.Windows网页设置2.1 Windows IIS功能设置2.2 IIS网页访问测试 3. Cpolar内网穿透3.1 下载安装Cpolar3.2 Cpolar云端设置3.3 Cpolar本地设置 4.公网访问测试5.结语 1.前言 在网上各种教程和介绍中&#xff0c;搭建网页都会借助各种软件的帮助&#xff0c;比如…

关于数据生成二维码保存和解密删除二维码

文章目录 前言一、pom配置依赖二、文件引入1.BufferedImageLuminanceSource2.QRCodeUtil3.MyPicConfig4.UploadUtils三、测试前言 所需文件: MyPicConfig 主要解决上传图片实时刷新BufferedImageLuminanceSource 算法文件QRCodeUtil 生成二维码工具类UploadUtils 主要解决上传…

Python中模块的动态导入和自动安装

前言 在 Python 开发中&#xff0c;正确管理和安装所需的第三方模块是至关重要的&#xff0c;但手动处理模块依赖可能会变得繁琐且容易出错。 为了简化这一过程&#xff0c;Python 提供了动态导入和自动安装模块的能力。本文将介绍如何使用动态导入和自动安装模块的方法&#x…

自学黑客技术很难吗?如何自学黑客技术

有人的地方就有江湖&#xff0c;有互联网江湖的地方就有web安全工程师的身影。随着移动互联网的快速发展&#xff0c;网络安全问题成为越来越重要的事情&#xff0c;但由于之前国家对网络安全的不重视&#xff0c;导致网络安全人才严重缺失&#xff0c;所以成为一名网络安全工程…

爱眼护眼的倡导者,康瞳护眼吧引领更多的人关注眼部健康

爱眼护眼的倡导者,康瞳护眼吧引领更多的人关注眼部健康#微信热点#康瞳护眼膏百收网SEO 大家早上好有好消息告诉大家 人民日报连续❷大版面报道&#x1f4f0; 关于 ❝青少年近视眼防控的宣传❞ ——降 低近视率迫在眉睫‼️ 轻体营开营倒计时 ⏰⏰3天⏰⏰ 来此一生&#x…