目录
system V共享内存
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。
共享内存示意图
共享内存在我们的虚拟地址空间的共享区当中,这意味着它一旦通信建立,一有数据就能一下被拿到。
共享内存函数
shmget函数
功能:用来创建共享内存 原型 int shmget(key_t key, size_t size, int shmflg); 参数 key:这个共享内存段名字 size:共享内存大小 shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的 返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1shmat函数
功能:将共享内存段连接到进程地址空间 原型 void *shmat(int shmid, const void *shmaddr, int shmflg); 参数 shmid: 共享内存标识 shmaddr:指定连接的地址 shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY 返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1说明:
shmaddr为NULL,核心自动选择一个地址 shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。 shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA) shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
shmdt函数
功能:将共享内存段与当前进程脱离 原型 int shmdt(const void *shmaddr); 参数 shmaddr: 由shmat所返回的指针 返回值:成功返回0;失败返回-1 注意:将共享内存段与当前进程脱离不等于删除共享内存段shmctl函数
功能:用于控制共享内存 原型 int shmctl(int shmid, int cmd, struct shmid_ds *buf); 参数 shmid:由shmget返回的共享内存标识码 cmd:将要采取的动作(有三个可取值) buf:指向一个保存着共享内存的模式状态和访问权限的数据结构 返回值:成功返回0;失败返回-1
代码示例
shm头文件
#ifndef _SHM_HPP_ #define _SHM_HPP_ #include <iostream> #include <cstdio> #include <cerrno> #include <string> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <fcntl.h> #include <sys/ipc.h> #include <sys/shm.h> #include <string.h> #define Creater 1 #define User 2 const int gShmSize = 4096; const std::string gpathname = "/home/lsf/lesson24/shm"; const int gproj_id = 0x66; class Shm { private: key_t GetCommKey() { key_t k = ftok(_pathname.c_str(), _proj_id); if (k < 0) { perror("ftok"); } return k; } int GetShmHelper(key_t key, int size, int flag) { int shmid = shmget(key, size, flag); if (shmid < 0) { perror("shmget"); } return shmid; } std::string RoleToString(int who) { if (who == Creater) return "Creater"; else if (who == User) return "User"; else return "None"; } void *AttachShm() { if (_addrshm != nullptr) DetachShm(_addrshm); void *shmaddr = shmat(_shmid, nullptr, 0); if (shmaddr == nullptr) { perror("shmat"); } std::cout << "who: " << RoleToString(_who) << " attach shm..." << std::endl; return shmaddr; } void DetachShm(void *shmaddr) { if (shmaddr == nullptr) return; shmdt(shmaddr); std::cout << "who: " << RoleToString(_who) << " detach shm..." << std::endl; } public: Shm(const std::string &pathname, int proj_id, int who) : _pathname(pathname), _proj_id(proj_id), _who(who), _addrshm(nullptr) { _key = GetCommKey(); if (_who == Creater) GetShmUseCreate(); else if (_who == User) GetShmForUse(); _addrshm = AttachShm(); std::cout << "shmid: " << _shmid << std::endl; std::cout << "_key: " << ToHex(_key) << std::endl; } std::string ToHex(key_t key) { char buffer[128]; snprintf(buffer, sizeof(buffer), "0x%x", key); return buffer; } bool GetShmForUse() { if (_who == User) { _shmid = GetShmHelper(_key, gShmSize, IPC_CREAT); // sleep(10); if (_shmid >= 0) { std::cout << "shm get done..." << std::endl; return true; } } return false; } bool GetShmUseCreate() { if (_who == Creater) { _shmid = GetShmHelper(_key, gShmSize, IPC_CREAT | IPC_EXCL | 0666); // sleep(10); if (_shmid >= 0) { std::cout << "shm create done..." << std::endl; return true; } } return false; } void *Addr() { return _addrshm; } void Zero() { if (_addrshm) { memset(_addrshm, 0, gShmSize); } } ~Shm() { if (_who == Creater) { int res = shmctl(_shmid, IPC_RMID, nullptr); } std::cout << "shm remove done..." << std::endl; } private: key_t _key; int _shmid; std::string _pathname; int _proj_id; int _who; void *_addrshm; }; #endif成员变量中除了_pathname和_who一个表示路径名一个表示身份外,其余的都是我们上文提到的函数的参数,我们遇到时一一讲解。
构造函数
Shm(const std::string &pathname, int proj_id, int who) : _pathname(pathname), _proj_id(proj_id), _who(who), _addrshm(nullptr) { _key = GetCommKey(); if (_who == Creater) GetShmUseCreate(); else if (_who == User) GetShmForUse(); _addrshm = AttachShm(); std::cout << "shmid: " << _shmid << std::endl; std::cout << "_key: " << ToHex(_key) << std::endl; }我们分别将我们的路径,_proj_id(后面说),身份,_addeshm这些能设的初始值先设置好。然后我们就可以先获得一个我们共享内存的key标识值,为什么不直接设置,而是要写个函数呢?这是为了达到我们唯一性的目的,我们程序员自己设置的话难免会设置到重复的,为了避免它我们就可以用一个ftok函数,它会根据我们的路径和_proj_id用一种算法设计出一个key值,我们采用它能达到我们心目中的要求。
它的返回值就是我们要的key值。
项目标识符(proj_id):除了文件路径外,
ftok()还需要一个项目标识符(proj_id)。这个标识符的最低有效8位将被用于生成键值。proj_id必须是非零的,以确保生成的键值不是全零(全零的键值在System V IPC中有特殊含义,通常表示无效的键值)。获取key值
key_t GetCommKey() { key_t k = ftok(_pathname.c_str(), _proj_id); if (k < 0) { perror("ftok"); } return k; }这就是我们的获取key值的函数,我们只是对ftok简单的封装了一下。
我们回到构造函数,在我们完成了key值的设置之后我们就要对创建者和使用则进行不同的构造方式了。
创建者的构造方式
int GetShmHelper(key_t key, int size, int flag) { int shmid = shmget(key, size, flag); if (shmid < 0) { perror("shmget"); } return shmid; } bool GetShmUseCreate() { if (_who == Creater) { _shmid = GetShmHelper(_key, gShmSize, IPC_CREAT | IPC_EXCL | 0666); // sleep(10); if (_shmid >= 0) { std::cout << "shm create done..." << std::endl; return true; } } return false; }GetShmHelper函数就是对我们的shmget函数做个简单封装,
GetShmHelper函数这个函数根据给定的键值(key)、大小(size)和标志(flag)来获取或创建一个共享内存段。
- 参数:
key_t key:共享内存段的键值,由ftok()函数生成。int size:共享内存段的大小(以字节为单位)。int flag:控制shmget()行为的标志。可以是IPC_CREAT(如果键不存在,则创建新的共享内存段)、IPC_EXCL(与IPC_CREAT一起使用时,如果键已存在,则调用失败)等标志的组合。- 返回值:
- 成功时返回共享内存段的标识符(
shmid)。- 失败时返回-1,并通过
perror()函数打印错误信息。
GetShmUseCreate函数这个函数检查某个条件(这里是通过
_who变量与Creater比较),如果条件满足,则尝试创建一个新的共享内存段。
- 变量:
_who:用于指示当前进程的角色。_key:共享内存段的键值。gShmSize:共享内存段的大小。_shmid:用于存储共享内存段标识符的变量。- 逻辑:
- 如果
_who等于Creater,则调用GetShmHelper函数尝试创建新的共享内存段。GetShmHelper的调用使用了IPC_CREAT | IPC_EXCL | 0666作为标志,这意味着如果键值已存在,则调用将失败(因为IPC_EXCL标志的存在)。0666是权限设置,但由于IPC_CREAT和IPC_EXCL的存在,它实际上只在创建新段时有效。- 如果
GetShmHelper返回非负值(即成功创建了共享内存段或获取了已存在的共享内存段),则函数返回true。- 如果
GetShmHelper返回-1(即失败),则函数返回false,并且不会打印"shm create done..."消息。使用者的构造方式
bool GetShmForUse() { if (_who == User) { _shmid = GetShmHelper(_key, gShmSize, IPC_CREAT); // sleep(10); if (_shmid >= 0) { std::cout << "shm get done..." << std::endl; return true; } } return false; }
GetShmForUse函数
目的:尝试获取一个共享内存段以供使用。当前角色是用户(
_who == User),尝试获取或创建共享内存段。参数:
- 隐式参数(全局变量):
_who:当前进程的角色。_key:共享内存段的键值。gShmSize:共享内存段的大小。_shmid:用于存储共享内存段标识符的变量。逻辑:
- 如果
_who等于User,则调用GetShmHelper函数尝试获取或创建共享内存段。GetShmHelper的调用使用了IPC_CREAT作为标志。这意味着:
- 如果键值对应的共享内存段已存在,则
shmget将返回该段的标识符。- 如果键值对应的共享内存段不存在,则
shmget将创建一个新的共享内存段,并返回其标识符。- 如果
GetShmHelper返回非负值(即成功获取了已存在的段或创建了新段),则函数返回true,打印"shm get done..."。- 如果
GetShmHelper返回-1(即失败,这通常不应该发生,因为IPC_CREAT允许创建新段)。- 如果
_who不是User,则函数直接返回false。分离附加操作
void DetachShm(void *shmaddr) { if (shmaddr == nullptr) return; shmdt(shmaddr); std::cout << "who: " << RoleToString(_who) << " detach shm..." << std::endl; } void *AttachShm() { if (_addrshm != nullptr) DetachShm(_addrshm); void *shmaddr = shmat(_shmid, nullptr, 0); if (shmaddr == nullptr) { perror("shmat"); } std::cout << "who: " << RoleToString(_who) << " attach shm..." << std::endl; return shmaddr; }
DetachShm函数
目的:分离(detach)之前附加(attach)的共享内存段。
参数:
shmaddr:指向之前附加的共享内存段的指针。逻辑:
- 如果传入的
shmaddr是nullptr,则函数直接返回,不执行任何操作。这是一种防御性编程实践,用于防止对空指针进行解引用。- 使用
shmdt函数分离共享内存段。shmdt接受一个指向共享内存段的指针,并分离该进程与该内存段之间的关联。- 打印一条消息,指示当前角色(通过
RoleToString(_who)获取)正在分离共享内存段。RoleToString是一个将角色转换为字符串的函数。
AttachShm函数
目的:附加(attach)到一个已存在的共享内存段。
参数:无。
逻辑:
- 如果全局变量(或类成员变量)
_addrshm不是nullptr,则首先调用DetachShm函数分离当前附加的共享内存段(如果有的话)。这是一种清理操作,确保在附加新的共享内存段之前不会泄漏旧的内存段。- 使用
shmat函数附加到共享内存段。shmat接受共享内存段的标识符、一个指向期望附加地址的指针(这里传入nullptr,让系统选择地址)、以及一组标志(这里传入0,表示使用默认行为)。- 如果
shmat返回nullptr,则使用perror函数打印一条错误消息。这意味着附加操作失败,可能是由于资源不足、权限问题或其他原因。- 打印一条消息,指示当前角色(通过
RoleToString(_who)获取)正在附加共享内存段。- 返回
shmat返回的指针,即指向附加的共享内存段的指针。如果shmat失败,则返回nullptr。RoleToString
std::string RoleToString(int who) { if (who == Creater) return "Creater"; else if (who == User) return "User"; else return "None"; }将_who转换成字符串。
接下来我们可以打印一下共享内存标识符和我们的key值,我们将key值转换成十六进制,方便观察。
std::string ToHex(key_t key) { char buffer[128]; snprintf(buffer, sizeof(buffer), "0x%x", key); return buffer; }void *Addr() { return _addrshm; }我们可以使用这个函数让上层获取共享内存的地址。
void Zero() { if (_addrshm) { memset(_addrshm, 0, gShmSize); } }我们可以用memset将这段共享内存清空。
析构函数
~Shm() { if (_who == Creater) { int res = shmctl(_shmid, IPC_RMID, nullptr); } std::cout << "shm remove done..." << std::endl; }这里我们也是让创建者把共享内存移除。
int res = shmctl(_shmid, IPC_RMID, nullptr);:这行代码调用shmctl函数尝试删除共享内存。_shmid是共享内存的标识符,IPC_RMID是删除共享内存的命令,nullptr是指向shmid_ds结构的指针(在这个命令中不需要,因此传递nullptr)。函数返回的结果存储在res变量中。客户端
#include "Shm.hpp" #include "namedPipe.hpp" int main() { //1.先创建共享内存 Shm shm(gpathname,gproj_id,User); shm.Zero(); char *shmaddr = (char*)shm.Addr(); sleep(3); //2.打开管道 NamePiped fifo(comm_path,User); fifo.OpenForWrite(); char ch = 'A'; while(ch <= 'Z') { shmaddr[ch-'A'] = ch; ch++; std::string temp = "weakup"; std::cout<< "add" <<ch<<" into shm, "<<"weakup reader"<<std::endl; fifo.WriteNamedPipe(temp); sleep(2); } return 0; }我们创建共享内存并清空原先的数据,获取一下共享内存地址,然后我们使用之前的管道,管道在这里的作用就是同步,因为我们的共享内存是直接内存访问,这意味着我们的客户端一发送服务端就能看到。所以我们需要借助管道的特性达到同步的效果,管道在这里发送什么信息并不重要。
服务端
#include "Shm.hpp" #include "namedPipe.hpp" int main() { //1.先创建共享内存 Shm shm(gpathname,gproj_id,Creater); char *shmaddr = (char*)shm.Addr(); //2.创建管道 NamePiped fifo(comm_path,Creater); fifo.OpenForRead(); while(true) { std::string temp; fifo.ReadNamedPipe(&temp); std::cout<<"shm memory content: " << shmaddr << std::endl; sleep(1); } sleep(5); return 0; }服务端的逻辑也是一样的,不同的是服务端是只读。
代码效果:
命名管道与共享内存的区别
命名管道与共享内存是两种常见的进程间通信(IPC)机制,它们各自具有独特的特点和适用场景。以下是两者的主要区别:
一、定义与工作原理
命名管道(Named Pipe):
- 也称为FIFO(First In First Out)管道。
- 它允许无亲缘关系的进程间进行通信,通过一个在文件系统中存在的名字来标识,进程可以通过这个名字来访问和通信。
- 数据流动是单向的,即数据只能从一个进程流向另一个进程。如果需要双向通信,通常需要创建两个管道。
- 命名管道常用于跨进程通信,并且允许数据以流的方式从一个进程传输到另一个进程。
共享内存(SharedMemory):
- 它是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。
- 它是针对其他进程间通信方式运行效率低而专门设计的,允许两个或多个进程共享一个给定的存储区。
- 一个进程写入共享内存的信息,可以被其他使用这个共享内存的进程通过一个简单的内存读取读出,从而实现了进程间的通信。
二、性能与效率
命名管道:
- 数据传输通过内核缓冲区进行,存在一定的开销。
- 相比其他IPC机制(如信号、消息队列等),命名管道的性能处于中等水平。
- 它支持阻塞和非阻塞操作,提供了灵活的通信方式。
共享内存:
- 是最快的IPC方式之一,因为它允许进程直接访问共享的内存区域,减少了数据的拷贝次数。
- 相比命名管道等机制,共享内存具有更高的数据传输效率和更低的延迟。
- 但是,共享内存需要额外的同步机制(如信号量)来避免数据竞争和不一致性问题。
三、使用场景与限制
命名管道:
- 适用于需要在不同进程间传递流式数据的场景。
- 由于其半双工特性和需要创建两个管道以实现双向通信的限制,命名管道在某些复杂通信场景中可能不够灵活。
共享内存:
- 适用于需要高效数据传输和大量数据共享的场景。
- 由于允许多个进程直接访问同一块内存区域,因此需要注意同步和互斥问题,以避免数据竞争和访问冲突。
四、实现与操作
命名管道:
- 在类Unix系统中,可以通过mkfifo函数创建命名管道,并使用open、read、write等系统调用进行读写操作。
- 在Windows系统中,命名管道通常通过特定的API进行创建和管理。
共享内存:
- 在类Unix系统中,可以使用shmget、shmat、shmdt、shmctl等系统调用进行共享内存的创建、挂接、去关联和控制操作。
- 在Windows系统中,也有相应的API用于共享内存的创建和管理。
综上所述,命名管道和共享内存是两种各具特色的进程间通信机制。命名管道适用于需要在不同进程间传递流式数据的场景,而共享内存则适用于需要高效数据传输和大量数据共享的场景。在选择使用哪种IPC机制时,需要根据具体的应用需求和场景进行权衡和选择。









