当前位置: 首页 > news >正文

Linux系统--进程间通信--共享内存(主使用)

Linux系统–进程间通信–共享内存(主使用)

前言

本文我们将接续上文Linux系统–进程间通信–共享内存(主概念和理解)-CSDN博客,本节主要通过代码来讲解共享内存的工作流程。

本文使用到的函数以及指令详解如下:

Linux系统–进程间通信–共享内存相关指令-CSDN博客

Linux系统–进程间通信–共享内存核心函数讲解-CSDN博客


下面是一个典型的、使用 System V 共享内存和信号量(简化示意)的完整流程:

完整代码

在Comm.hpp中

// 注意了这里的路径是我的路径,使用的时候大家要切换成自己的路径
const char *pathname = "/home/lx/working/d47/SharedMemory";

# Makefile
.PHONY:all
all:shm_client shm_servershm_client:ShmClient.ccg++ -o $@ $^ -std=c++11shm_server:ShmServer.ccg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f shm_client shm_server

// Comm.hpp
#pragma once#include <iostream>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <string>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>using namespace std;// pathname和proj_id是用来生成key的
// 注意了这里的路径是我的路径,使用的时候大家要切换成自己的路径
const char *pathname = "/home/lx/working/d47/SharedMemory";
const int proj_id = 0x66;// 在内核中,共享内存的大小是以4KB为基本单位的. 你只能用你申请的大小。建议申请大小是n*4KB
const int defaultsize = 4096; // 单位是字节// 这个函数的功能主要是:把十进制的数字以十六进制的形式打印出来
// %x就是以16进制打印的意思
// 不过由于snprintf打印出来的16进制数字前面没有0x,容易和十进制数字混淆,所以我们用0x表示这个是十六进制数
// 而且这里snprintf打印函数的作用并不是向屏幕打印,而是将格式化后的数据写入buffer函数.
// 我们为什么需要这个功能呢?因为后续演示使用指令打印共享内存的信息,系统用的就是16进制,所以这里我们也使用16进制,这样好看些
std::string ToHex(key_t k)
{char buffer[1024];snprintf(buffer, sizeof(buffer), "0x%x", k);return buffer;
}// 这个函数我们封装了ftok函数
// 用来创建key
key_t GetShmKeyOrDie()
{key_t k = ftok(pathname, proj_id);if (k < 0){// 创建不成功我们可以打印一下错误信息std::cerr << "ftok error, errno : " << errno << ", error string: " << strerror(errno) << std::endl;// 如果创建不成功,进程直接退出exit(1);}return k;// 最后返回创建出来的key
}// 这个函数我们封装了shmget函数,用来创建/获取共享内存
int CreateShmOrDie(key_t key, int size, int flag)
{int shmid = shmget(key, size, flag);if (shmid < 0){// 如果创建/获取不成功,我们打印错误信息std::cerr << "shmget error, errno : " << errno << ", error string: " << strerror(errno) << std::endl;exit(2);}return shmid;// 返回标识共享内存段的shmid
}// 这个函数主要是服务器端调用,我们是让服务器端完成共享内存的创建
// 封装的CreateShmOrDie函数.
int CreateShm(key_t key, int size)
{// IPC_CREAT: 不存在就创建,存在就获取// IPC_EXCL: 必须与 IPC_CREAT 一起使用。如果共享内存已存在,则调用失败。// IPC_CREAT | IPC_EXCL: 不存在就创建,存在就出错返回return CreateShmOrDie(key, size, IPC_CREAT | IPC_EXCL | 0666);
}//这个函数主要是客户端调用,让客户端获取服务器端创建好的共享内存段
int GetShm(key_t key)
{return CreateShmOrDie(key, 0, 0);
}// 这个函数我们封装了shmctl, 用来删除共享内存
void DeleteShm(int shmid)
{int n = shmctl(shmid, IPC_RMID, nullptr);if (n < 0){// 如果删除失败,就打印错误信息std::cerr << "shmctl error" << std::endl;}
}// 这个函数主要用来打印共享内存段的信息
// 封装了shmctl函数.
void ShmDebug(int shmid)
{struct shmid_ds shmds;int n = shmctl(shmid, IPC_STAT, &shmds);if (n < 0){// 失败则打印错误信息std::cerr << "shmctl error" << std::endl;return;}std::cout << "shmds.shm_segsz: " << shmds.shm_segsz << std::endl;std::cout << "shmds.shm_nattch:" << shmds.shm_nattch << std::endl;std::cout << "shmds.shm_ctime:" << shmds.shm_ctime << std::endl;std::cout << "shmds.shm_perm.__key:" << ToHex(shmds.shm_perm.__key) << std::endl;
}// 这个函数主要是用来创建虚拟地址空间中虚拟地址到物理内存中的共享内存的映射
// 也就是将共享内存附加(挂载)到进程上
// 封装的是shmat函数
void *ShmAttach(int shmid)
{void *addr = shmat(shmid, nullptr, 0);if ((long long int)addr == -1){// 失败则打印错误信息std::cerr << "shmat error" << std::endl;return nullptr;}return addr;
}// 封装shmdt函数,用来分离共享内存
void ShmDetach(void *addr)
{int n = shmdt(addr);if (n < 0){std::cerr << "shmdt error" << std::endl;}
}

// Fifo.hpp
#ifndef __COMM_HPP__
#define __COMM_HPP__#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <cassert>using namespace std;#define Mode 0666
#define Path "./fifo"class Fifo
{
public:// 通过构造函数,创建管道文件Fifo(const string &path = Path) : _path(path){umask(0);int n = mkfifo(_path.c_str(), Mode);if (n == 0){cout << "mkfifo success" << endl;}else{cerr << "mkfifo failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;}}// 通过析构函数,删除管道文件~Fifo(){int n = unlink(_path.c_str());if (n == 0){cout << "remove fifo file " << _path << " success" << endl;}else{cerr << "remove failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;}}private:string _path; // 文件路径+文件名
};class Sync
{
public:Sync() : rfd(-1), wfd(-1){}// 以读方式打开管道文件void OpenReadOrDie(){cout<<"以读方式打开管道文件,服务器端准备阻塞"<<endl;rfd = open(Path, O_RDONLY);if (rfd < 0)exit(1);// 如果打开失败直接退出进程}// 以写方式打开管道文件void OpenWriteOrDie(){cout<<"以写方式打开管道文件,服务器将脱离阻塞"<<endl;wfd = open(Path, O_WRONLY);if (wfd < 0)exit(1);// 如果打开失败直接退出进程}// 借助命名管道的阻塞特性,如果管道为空,执行读操作的进程会阻塞bool Wait(){bool ret = true;uint32_t c = 0;ssize_t n = read(rfd, &c, sizeof(uint32_t));// 从管道中读取数据if (n == sizeof(uint32_t)){// 读取成功,我们打印信息,提醒服务器端(往往是读端)已经被唤醒,开始读取数据std::cout << "server wakeup, begin read shm..." << std::endl;}else if (n == 0){// 这个是因为管道的写端已经全部关闭,read函数的返回值为0,提醒现在已经可以关闭读端了ret = false;}else{return false;}return ret;}// 我们主要是通过写操作来唤醒阻塞中的读端(服务器端)void Wakeup(){uint32_t c = 0;ssize_t n = write(wfd, &c, sizeof(c));assert(n == sizeof(uint32_t));// 判断是否写入失败// 完成写入后,打印信息,提醒用户,服务器端已经被唤醒std::cout << "wakeup server..." << std::endl;}~Sync() {}private:int rfd;int wfd;
};#endif

// ShmServer.cc // 服务器端
#include "Comm.hpp"
#include "Fifo.hpp"
#include <unistd.h>int main()
{// 1. 获取keykey_t key = GetShmKeyOrDie();std::cout << "key: " << ToHex(key) << std::endl;// sleep(2);// 2. 创建共享内存int shmid = CreateShm(key, defaultsize);std::cout << "shmid: " << shmid << std::endl;// sleep(2);// ShmDebug(shmid);// 4. 将共享内存和进程进行挂接(关联)char *addr = (char *)ShmAttach(shmid);std::cout << "Attach shm success, addr: " << ToHex((uint64_t)addr) << std::endl;// 0. 先引入管道Fifo fifo;// 创建管道文件Sync syn;// 借助命名管道的同步机制间接的给共享内存提供同步机制// 先以读方式打开管道文件,如果写端(客户端)没有打开管道文件则会在这里阻塞住// 直到客户端以写方式打开管道文件syn.OpenReadOrDie();// 客户端打开管道文件其实也就是意味着,客户端已经获取并挂载并初始化了共享内存,可以通过共享内存进行通信cout<<"客户端打开管道文件,可以开始通信"<<endl;// 可以进行通信了for(;;){// 如果读操作也就是read函数,返回值是0,说明客户端已经关闭管道文件,全部写端已经关闭,这个时候我们就可以选择退出了// 或者说read操作失败了,我们也直接退出if(!syn.Wait()) break;// 如果等待成功,我们就打印共享内存中的数据cout << "shm content: " << addr << std::endl;}// 分离共享内存ShmDetach(addr);std::cout << "Detach shm success, addr: " << ToHex((uint64_t)addr) << std::endl;// 3. 删除共享内存DeleteShm(shmid);return 0;
}

// ShmClient.cc // 客户端
#include "Comm.hpp"
#include "Fifo.hpp"
#include <unistd.h>int main()
{// 客户端也是先获取key,这样接下来才能获取到服务器端创建的共享内存key_t key = GetShmKeyOrDie();std::cout << "key: " << ToHex(key) << std::endl;// sleep(2);// 获取共享内存int shmid = GetShm(key);std::cout << "shmid: " << shmid << std::endl;// sleep(2);// 挂载(附加)共享内存到本进程char *addr = (char *)ShmAttach(shmid);std::cout << "Attach shm success, addr: " << ToHex((uint64_t)addr) << std::endl;// sleep(5);// 初始化开辟好了的共享内存,准备开始往共享内存中写入数据memset(addr, 0, defaultsize);Sync syn;// 借助命名管道实现的同步机制syn.OpenWriteOrDie();// 以写方式打开管道文件,唤醒阻塞在open函数处的读端(服务器端)// 可以进行通信了for (char c = 'A'; c <= 'Z'; c++) // pipe, fifo, ->read/write->系统调用, shm -> 没有使用系统调用!!{addr[c - 'A'] = c;// 向共享内存写入数据sleep(1);syn.Wakeup();// 向共享内存写入完数据后,就可以通过向管道文件写入数据,唤醒阻塞在read(读操作)的服务器端(读端)}// 通信完成后,分离共享内存// 删除共享内存是服务器端完成的ShmDetach(addr);std::cout << "Detach shm success, addr: " << ToHex((uint64_t)addr) << std::endl;sleep(5);return 0;
}

代码讲解

首先这个代码实现的依旧是客户端–服务端模式。两端使用共享内存进行通信。共享内存的通信方式/流程,我在代码中都有注释,在上文Linux系统–进程间通信–共享内存(主概念和理解)-CSDN博客中的第七点我也详细讲解了使用共享内存进行通信的完整流程。

这份代码唯一有一点大家可能比较疑惑的就是同步机制部分。由于我还没有学信号量,且信号量使用比较复杂,我们就没有使用信号量来实现同步机制。

核心思想:使用命名管道(FIFO)来实现一个简单的同步机制,以解决共享内存的竞态条件问题。其核心是利用了FIFO的阻塞特性:服务器端(读端)会在打开管道和读取管道时阻塞,等待客户端(写端)完成相应操作后将其唤醒,从而确保读写顺序:

我们让服务器端在创建共享内存后创建一个管道文件。让服务器端以读方式打开这个管道文件,默认情况下,以只读方式打开会阻塞直到有写端打开

如果没有借助命名管道或者别的方式来实现同步机制,那么服务器端在共享内存创建并挂载好后,服务器端会直接开始从共享内存中一直读取数据,就算客户端(写端)还没获取共享内存,或者客户端还没有往其中写入数据。

但我们借助命名管道的阻塞特性,服务器端就会等待客户端获取共享内存(客户端会以写方式打开管道文件,这样服务器端就会被唤醒),服务器端才被唤醒准备从共享内存中读取数据。

当两端都已经获取共享内存后(同时也都打开了管道文件),服务器端就从管道文件中读取数据(从空管道中读取数据,当前进程会进入阻塞状态),这样我们就能实现当写端(客户端)还没往共享内存中写入数据时服务器端阻塞等待。

当客户端往共享内存中写入数据后,客户端同时也向管道文件写入数据,唤醒阻塞中的服务器端,从而让服务端获取到客户端传送来的数据。


最后我们运行给程序来给大家梳理一遍程序的运行逻辑:

在这里插入图片描述


在这里插入图片描述


在这里插入图片描述

http://www.dtcms.com/a/473711.html

相关文章:

  • BOOST电路的一些小理解
  • JavaWeb登录模块完整实现解析:从前端点击到后端验证的全流程
  • 【pytorch】合并与分割
  • 从AI画稿到3D虚拟时装:Illustrator与Substance 3D的服装设计工作流
  • 【VGGT-X】:尝试将VGGT用到3DGS重建中去
  • 海珠区建设和水务局网站网站建设夜猫
  • 用 Go 优雅应对网络抖动与断线重连:打造健壮的网络应用
  • C++ : 智能指针的补充和特殊类的设计
  • 【完整源码+数据集+部署教程】 航拍水体检测图像分割系统源码和数据集:改进yolo11-DLKA
  • 公司查询网站查询系统景点介绍网站开发设计
  • 如何定位 TCP TIME_WAIT ,并优化这个问题
  • DDD记账软件实战四|从0-1设计实现企业级记账微服务
  • 考研408《计算机组成原理》复习笔记,第七章(1)——I/O接口
  • 建设部网站在哪里报名考试大德通网站建设
  • Java 泛型基础:从类型安全到泛型类 / 方法 / 接口全解析
  • git 绑定多个远程仓库指定推送场景
  • 前端学习2:学习时间3-4小时
  • setup与选项式API
  • 后端开发是什么:从服务器到数据库
  • 南宁3及分销网站制作大连建设网信息公开
  • 神经网络中的非线性激活函数:从原理到实践
  • 【IO多路复用】原理与选型(select/poll/epoll 解析)
  • AI 与神经网络:从理论到现代应用
  • 消息积压的问题如何解决
  • 神经网络常用激活函数公式
  • 回归预测 | MATLAB实现CNN(卷积神经网络)多输入单输出+SHAP可解释分析+新数据预测
  • 中国十大旅游网站wordpress视频试看付费
  • Docker部署的gitlab升级的详细步骤(升级到17.6.1版本)
  • 一个基于稀疏混合专家模型(Sparse Mixture of Experts, Sparse MoE) 的 Transformer 语言模型
  • Litho项目架构解析:四阶段流水线如何实现自动化文档生成