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

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

目录

一、System V通信

二、System V共享内存

1、共享内存的介绍

2、共享内存的原理

3、共享内存的函数

3.1 shmget

3.2 shmat

3.3 shmdt

3.4 shmctl

4、shmid和key的区别

5、共享内存与管道的对比


一、System V通信

# 前面我们所探究的通信方式都是基于管道文件的,而接下来我们将谈论的是在System V标准下,进程间通信的方式。

# System V标准是在同一主机内的进程间通信方案,是站在OS层面专门为进程间通信设计的方案,其主要提供三个主流方案:system V共享内存,system V消息队列,system V信号量

# 其中System V 共享内存和 System V 消息队列的目的在于传送数据。而 System V 信号量则是为确保进程间的同步与互斥而设计,虽然 System V 信号量看似与通信没有直接关联,但实际上它也属于通信范畴。


二、System V共享内存

1、共享内存的介绍

# 共享内存本质就是让不同进程间能够访问同一块物理空间,从而实现进程间通信。

2、共享内存的原理

# 我们之前动态库可以加载一份物理内存空间,然后通过页表映射,就可以映射到不同的进程的进程虚拟空间,所以实现不同进程的代码共享,而共享内存的原理也是类似的。

# 共享内存的实现方式是在物理内存中申请一块内存空间。接着,将这块内存空间与各个进程各自的页表分别建立映射,并在虚拟地址空间的共享区开辟空间,把虚拟地址填充到页表对应位置,从而建立虚拟地址与物理地址的对应关系。此时,不同进程便能访问同一份物理内存,此物理内存即共享内存。

# 所以我们通过一个内核结构体来描述共享内存,再由操作系统统一管理这些内核结构体,共享内存数据结构:

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

3、共享内存的函数

# 一般来说我们创建共享内存大致可以分为两步:第一步就是在物理内存上申请共享内存。第二步将申请到的物理内存挂接到对应的地址空间上。这两步都分别对应两个函数:shmgetshmat。如果想释放共享内存,步骤就刚好相反,首先第一步将共享内存与地址空间去关联,即取消映射关系,第二步将释放共享内存空间,即将物理内存归还给系统。这两步都分别对应两个函数:shmdtshmctl

3.1 shmget

# shmgetSystem V IPC创建或获取共享内存段的核心函数,其本质是向内核申请一块多进程可共同访问的物理内存区域。

  • 头文件:#include <sys/ipc.h>     #include <sys/shm.h>
  • 函数原型:int shmget(key_t key, size_t size, int shmflg);
  • 参数:第一个参数key,表示待创建共享内存在系统当中的唯一标识。第二个参数size,表示待创建共享内存的大小。第三个参数shmflg,表示创建共享内存的方式。
  • 返回值:shmget调用成功,返回一个有效的共享内存标识符(用户层标识符),否则返回-1。

# 首先shmget需要传入的第一个参数key需要通过函数ftok获取,其原型如下:

  • 头文件:#include <sys/types.h>     #include <sys/ipc.h>
  • 函数原型:key_t ftok(const char *pathname, int proj_id);

# 其中pathname代表一个已存在的路径名,proj_id代表一个项目IDftok函数可以将通过特定的算法将这两个参数转换出对应的系统标识符key,否则返回-1。

# 值得注意的是:

  1. 使用ftok函数生成key值可能会产生冲突,此时需要对传入ftok函数的参数进行修改。
  2. 如果不同进程间需要通信,需要采用同样的路径名和和项目ID,进而生成同一个key值,才能找到同一个共享资源。

# 第二个参数size一般建议是4096的整数倍,假设如果传的是4097,操作系统实际上申请的空间大小是 4096*2,虽然操作系统多申请了,但是多余的部分用户不能使用,这样就可能造成空间的浪费。

# 第三个参数shmflag标记位常用选项有两种:

  1. IPC_CREAT :如果申请的共享内存不存在,就创建,存在,就获取并返回。
  2. IPC_EXCL :如果申请的共享内存存在,就出错返回。

# IPC_CREAT | IPC_EXCL :如果申请的共享内存不存在,就创建,存在就出错返回。这俩选项一起使用保证了,如果我们申请成功了一个共享内存,这个共享内存一定是一个新的。

#pragma once#include <iostream>
#include <stdio.h>
#include <string>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/ipc.h>const int gdefaultid = -1;
const int gsize = 4096;
const std::string pathname = ".";
const int projid = 0x66;#define ERR_EXIT(m)         \do                      \{                       \perror(m);          \exit(EXIT_FAILURE); \} while (0)class Shm
{
public:Shm():_shmid(gdefaultid),_size(gsize){}// 创建的一定要是一个全新的共享内存void Creat(){key_t k = ftok(pathname.c_str(), projid);if(k < 0){ERR_EXIT("ftok");exit(EXIT_FAILURE);}printf("key: 0x%x", k);_shmid = shmget(k, _size, IPC_CREAT | IPC_EXCL);if(_shmid < 0){ERR_EXIT("shmget");}printf("shmid: %d", _shmid);}~Shm(){}private:int _shmid;int _size;
};

# 并且我们也可能通过指令ipcs - m查看相关信息。

# 其分别每一项的含义:

标题含义
key系统区别各个共享内存的唯一标识
shmid共享内存的用户层 id
owner共享内存的拥有者
perms共享内存的权限
bytes共享内存的大小
nattch关联共享内存的进程数
status共享内存的状态

# 因为共享内存的生命周期是随内核的,所以如果不手动回收,这个共享内存就会一直存在。除了通过特定的函数外,我们也能够通过指令ipcrm -m shmid释放指定的共享内存资源。

# 注意不能通过key删除共享内存,因为key只是给内核来区分的唯一性的。而我们的指令本质是运行在用户空间的,而用户层只能使用shmid访问共享内存,而代码级别删除共享内存的系统调用是shmctl

3.2 shmat

# shmat函数可以将共享内存映射到进程的虚拟地址空间,使进程可访问共享数据。

  • 头文件:#include <sys/types.h>     #include <sys/shm.h>
  • 函数原型:void *shmat(int shmid, const void *shmaddr, int shmflg);
  • 参数:第一个参数shmid,表示待关联共享内存的用户级标识符。第二个参数shmaddr,指定共享内存映射到进程地址空间的某一地址,通常设置为NULL,表示让内核自己决定一个合适的地址位置。第三个参数shmflg,表示关联共享内存时设置的某些属性。
  • 返回值:shmat调用成功,返回共享内存映射到进程地址空间中的起始地址。否则,返回(void*)-1。

# 一般shmat函数的第三个参数传入的常用的选项有以下三种:

  1. SHM_RDONLY:关联共享内存后只进行只读操作。
  2. SHM_RND:若shmaddr不为NULL,则关联地址自动向下调整为SHMLBA(通常为页大小)的整数倍。
  3. 0:默认为读写权限。

// Comm.hpp#pragma once#include <cstdio>
#include <cstdlib>#define ERR_EXIT(m)         \
do                          \
{                           \perror(m);              \exit(EXIT_FAILURE);     \
} while (0)
// Shm.hpp#pragma once#include <iostream>
#include <stdio.h>
#include <string>
#include <unistd.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/ipc.h>#include "Comm.hpp"const int gdefaultid = -1;
const int gsize = 4096;
const std::string pathname = ".";
const int projid = 0x66;
const int gmode = 0666;#define CREATER "creater"
#define USER "user"class Shm
{
private:// 创建的一定要是一个全新的共享内存void CreateHelper(int flg){printf("key: 0x%x\n", _key);// _shmid = shmget(k, _size, IPC_CREAT | IPC_EXCL | gmode);_shmid = shmget(_key, _size, flg);if (_shmid < 0){ERR_EXIT("shmget");}printf("shmid: %d\n", _shmid);}void Create(){CreateHelper(IPC_CREAT | IPC_EXCL | gmode);}void Attach(){_start_mem = shmat(_shmid, nullptr, 0);if ((long long)_start_mem < 0){ERR_EXIT("shmat");}printf("attach success\n");}void Get(){CreateHelper(IPC_CREAT);}// 共享内存通常需要在所有进程使用完毕后才能删除,而非某个进程的对象销毁时。如果在析构函数中删除,可能导致 “提前删除”(例如一个进程只是临时使用完对象,但其他进程仍在访问)。void Destroy(){// if (_shmid == gdefaultid)//     return;int n = shmctl(_shmid, IPC_RMID, nullptr);if (n > 0){printf("shmctl delete shm: %d success!\n", _shmid);}else{ERR_EXIT("shmctl");}}public:Shm(const std::string &pathname, int projid, const std::string &usertype):_shmid(gdefaultid),_size(gsize),_start_mem(nullptr),_usertype(usertype){_key = ftok(pathname.c_str(), projid);if (_key < 0){ERR_EXIT("ftok");}if(_usertype == CREATER)Create();else if(_usertype == USER)Get();else{}Attach();}int Size(){return _size;}void *VirtualAddr(){printf("VirtualAddr: %p\n", _start_mem);return _start_mem;}~Shm(){// if(_usertype == CREATER)Destroy();}private:int _shmid;key_t _key;int _size;void *_start_mem;std::string _usertype;
};
// Fifo.hpp#pragma once#include <iostream>
#include <string>
#include <cstdio>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>#include "Comm.hpp"#define PATH "."
#define FILENAME "fifo"class NamedFifo
{
public:NamedFifo(const std::string &path, const std::string &name): _path(path), _name(name){_fifoname = _path + "/" + _name;// 将文件默认掩码设置为0umask(0);// 新建管道int n = mkfifo(_fifoname.c_str(), 0666);if (n < 0){// std::cerr << "mkfifo error" << std::endl;// perror("mkfifo");// exit(1);ERR_EXIT("mkfifo");}else{std::cout << "mkfifo success" << std::endl;}}~NamedFifo(){// 删除管道文件int n = unlink(_fifoname.c_str());if (n == 0){std::cout << "remove fifo success" << std::endl;}else{// std::cout << "remove fifo failed" << std::endl;ERR_EXIT("unlink");}}private:std::string _path;std::string _name;std::string _fifoname;
};class FileOper
{
public:FileOper(const std::string &path, const std::string &name): _path(path), _name(name), _fd(-1){_fifoname = _path + "/" + _name;}void OpenForRead(){// 打开// write方没有执行open的时候,read方就要在open内部进行阻塞,直到有人把管道文件打开了,open才会返回_fd = open(_fifoname.c_str(), O_RDONLY); // 以读方式打开命名管道文件if (_fd < 0){// std::cerr << "open fifo error" << std::endl;// return;ERR_EXIT("open");}std::cout << "open fifo success" << std::endl;}void OpenForWrite(){// write_fd = open(_fifoname.c_str(), O_WRONLY); // 以写方式打开命名管道文件if (_fd < 0){// std::cerr << "Open fifo error" << std::endl;// return;ERR_EXIT("open");}std::cerr << "Open fifo success" << std::endl;}void Wakeup(){// 写入操作char c = 'c';int n = write(_fd, &c, 1);printf("唤醒: %d\n", n);}bool Wait(){char c;int number = read(_fd, &c, 1);if(number > 0){printf("醒来: %d\n", number);return true;}elsereturn false;}void Close(){close(_fd);}~FileOper(){}private:std::string _path;std::string _name;std::string _fifoname;int _fd;
};
// server.cc#include "Shm.hpp"
#include "Fifo.hpp"int main()
{Shm shm(pathname, projid, CREATER);// 创建管道文件NamedFifo fifo(PATH, FILENAME);// 文件操作FileOper readerfile(PATH, FILENAME);readerfile.OpenForRead();char *mem = (char*)shm.VirtualAddr();while(true){if(readerfile.Wait()) // 默认会阻塞{printf("%s\n", mem);}elsebreak;}readerfile.Close();return 0;
}

// client.cc#include "Shm.hpp"
#include "Fifo.hpp"int main()
{FileOper writerfile(PATH, FILENAME);writerfile.OpenForWrite();Shm shm(pathname, projid, USER);char *mem = (char*)shm.VirtualAddr();int index = 0;for (char c = 'A'; c <= 'Z'; c++, index += 2){// 才是向共享内存里写入sleep(1);mem[index] = c;mem[index + 1] = c;sleep(1);mem[index + 2] = 0;writerfile.Wakeup();}writerfile.Close();return 0;
}

3.3 shmdt

将共享内存段从当前进程的地址空间分离(解除映射),但不会删除共享内存

  1. 头文件:#include <sys/types.h>     #include <sys/shm.h>
  2. 函数原型:int shmdt(const void *shmaddr);
  3. 参数:shmaddr为待去关联共享内存的起始地址,即调用shmat函数时得到的返回值。
  4. 返回值:shmdt调用成功,返回0。否则返回-1。

3.4 shmctl

管理共享内存段,包括删除、状态查询或权限修改。

  1. 头文件:#include <sys/ipc.h>     #include <sys/shm.h>
  2. 函数原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf);
  3. 参数:第一个参数shmid,表示所控制共享内存的用户级标识符。第二个参数cmd,表示具体的控制动作。第三个参数buf,用于获取或设置所控制共享内存的数据结构。
  4. 返回值:shmctl调用成功,返回0。否则返回-1。

# 其中第二个选项cmd常见有三个选项:

  1. IPC_STAT:获取共享内存的当前关联值,此时参数buf作为输出型参数。
  2. IPC_SET:在进程有足够权限的前提下,将共享内存的当前关联值设置为buf所指的数据结构中的值.
  3. IPC_RMID:删除共享内存段,此时buf可以传NULL

4、shmid和key的区别

5、共享内存与管道的对比

# 同样是实现客户端client与服务端server的交互,共享内存明显会比管道通信快的多。

# 从上图观察我们就可以看出,管道通信将一个文件内容从服务端发送到客户端一共需要四次拷贝:

  1. 服务端将信息从输入文件复制到服务端的临时缓冲区中。
  2. 将服务端临时缓冲区的信息复制到管道中。
  3. 客户端将信息从管道复制到客户端的缓冲区中。
  4. 将客户端临时缓冲区的信息复制到输出文件中。

# 而如果是共享内存却只需要两次拷贝即可,大大提升效率。

  1. 从输入文件到共享内存。
  2. 从共享内存到输出文件。

# 但是共享内存也有明显的缺陷,那就是没有同步与互斥这样的保护机制。

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

相关文章:

  • 基于llama.cpp在CPU环境部署Qwen3
  • JimuReport 积木报表 v2.1.3 版本发布,免费开源的可视化报表和大屏
  • 【Linux手册】Unix/Linux 信号:原理、触发与响应机制实战
  • 开源 C# .net mvc 开发(九)websocket--服务器与客户端的实时通信
  • Unity:XML笔记
  • 【基础】Three.js中如何添加阴影(附案例代码)
  • 基于SpringBoot的运动服装销售系统【2026最新】
  • 大型语言模型微调 内容预告(69)
  • 剧本杀小程序系统开发:重塑社交娱乐新生态
  • Trae x MCP:一键打造品牌专属高质量SVG封面
  • apipost 8.x 脚本循环调用接口
  • 9月1日
  • WhatsApp 漏洞与 Apple 零日漏洞一起被利用于间谍软件攻击
  • LangChain VectorStores核心:多向量数据库统一交互层与RAG存储中枢
  • 深度学习——速问速答
  • Java视觉跟踪入门:使用OpenCV实现实时对象追踪
  • Vue2存量项目国际化改造踩坑
  • pyside6小项目:进制转换器
  • 《架构师手记:SpringCloud整合Nacos实战·一》
  • 2.MySQL库的操作
  • Spark实现推荐系统中的相似度算法
  • 【LeetCode】19、删除链表的倒数第N个结点
  • P1803 凌乱的yyy / 线段覆盖
  • 802.11 和 802.1X
  • 计算机毕设选题:基于Python+Django的健康饮食管理系统设计【源码+文档+调试】
  • 网络原理——TCP/UDP/IP
  • 【面试场景题】如何快速判断几十亿个数中是否存在某个数
  • 【面试场景题】100M网络带宽能不能支撑QPS3000
  • (3dnr)多帧视频图像去噪 (一)
  • 第六章 Vue3 + Three.js 实现高质量全景图查看器:从基础到优化