共享内存的通信
目录
共享内存的挂接与取消挂接
server.cc / clent.cc /测试
IPC通信
使用命名管道保护
我们今天学习共享内存的通信!!!
共享内存的挂接与取消挂接
我们之前创建了共享内存,但是没有和进程想关联,没有关联进程这么调度,所以我们需要映射挂接到自己的目录,使用shmat()函数实现挂接:
三个参数,第一个就是shmget的返回值也就是ID,第二个参数是本地的虚拟地址,一般不需要自己设置,第三个参数一般设置为0,详情见上面。
取消挂接我们使用shmdt()函数,shmdt
(Shared Memory Detach)是 System V 共享内存(Shared Memory)机制中的一个函数,用于将共享内存段从当前进程的地址空间分离(取消映射)。
一个参数就是shmat成功的返回值。
然后运行server并监视共享内存看结果:
咦,这么什么都没有,nattch这么还是0,不是应该有依赖了吗,重点不再nattch,这个perms这么也是0,在 ipcs
命令的输出中,perms
代表 IPC 资源的权限(Permissions),用于控制进程对共享内存、信号量或消息队列的访问权限。哦我们没有设置共享内存的权限导致的。
我们可以在创建共享内存的时候进行创建,shmflg可以设置权限如下:
设置为仅自己可读写,然后所有进程可见。
可以看到正常挂接并且解除了,我们回过头来看操作系统,挂接就需要虚拟地址申请空间,操作系统申请空间是按照块为单位申请的,要么4KB,2KB等等,假设我们的系统每次申请空间是4KB,4096个字节,那如果我们今天的共享内存设置成4098,那操作系统是不是需要申请8kb才可以装下,bytes下面应该是8kb。但是操作系统不会故意迎合人的思维和行为而创建,操作系统实际上是创建了8kb没错,但是仍然会显示4098做为实际占有,你在 ipcs -m
里看到的共享内存大小 不是你创建时指定的大小,而是操作系统按页对齐后的实际分配大小。
那我们挂接完了就剩下通信了,但是目前这么写代码有点难看呀!,我们需要进行封装,为了使接口共享出来,我们将comm.hpp改为sharememory.hpp,作为共享空间,将一些公共的代码共享出来。如下:
sharememoty.hpp编写
具体框架如上,不用我过多解释了吧,清楚明了了。接着填写进去就可以了。值得注意的是,这个getshm也是获取shmid,但是里面的shmflg的选项不能含有IPC_EXCL,因为这个函数是获取已有的共享内存的shmid。
class sharememory
{
public:
sharememory();
~sharememory();
// 创建shm
int creatshm()
{
umask(0);
key_t k = ::ftok(path.c_str(), projid);
if (k < 0)
{
cerr << "ftok fail!" << endl;
return -1;
}
cout << "k: " << k << endl;
// 创建共享内存
int shmid = ::shmget(k, gshmsize, IPC_CREAT | IPC_EXCL | gmode);
if (shmid < 0)
{
cerr << "shmget error" << endl;
return -2;
}
cout << "shmid: " << shmid << endl;
return shmid;
}
// 得到shm,已有的获取不创建
int getshm()
{
key_t k = ::ftok(path.c_str(), projid);
if (k < 0)
{
cerr << "ftok fail!" << endl;
return -1;
}
cout << "k: " << k << endl;
// 创建共享内存
int shmid = ::shmget(k, gshmsize, IPC_CREAT | gmode);
if (shmid < 0)
{
cerr << "shmget error" << endl;
return -2;
}
cout << "shmid: " << shmid << endl;
return shmid;
}
// 挂接本地地址
void *attachshm(int shmid)
{
void* ret = shmat(shmid, nullptr, 0);
if ((long long)ret == -1)
{
return nullptr;
}
cout << "attach done" << (long long)ret << endl;
return ret;
}
// 解除映射
void detachshm(void* ret)
{
::shmdt(ret);
cout << "detach done: " << (long long)ret << endl;
}
// 删除shm
void deleteshm(int shmid)
{
shmctl(shmid, IPC_RMID, nullptr);
}
};
我们看到创建shmid和得到的函数高度一致,所以我们可以将其设置为私有函数,然后再调用,放在冗余。然后shmflg部分完全可以由用户自己传递。注意参数传递的不一致!!!
class sharememory
{
private:
int creatshmhelper(int shmflg)
{
umask(0);
key_t k = ::ftok(path.c_str(), projid);
if (k < 0)
{
cerr << "ftok fail!" << endl;
return -1;
}
cout << "k: " << k << endl;
// 创建共享内存
int shmid = ::shmget(k, gshmsize, shmflg);
if (shmid < 0)
{
cerr << "shmget error" << endl;
return -2;
}
cout << "shmid: " << shmid << endl;
return shmid;
}
public:
sharememory();
~sharememory();
// 创建shm
int creatshm()
{
return creatshmhelper(IPC_CREAT | IPC_EXCL | gmode);
}
// 得到shm,已有的获取不创建
int getshm()
{
return creatshmhelper(IPC_CREAT | gmode);
}
// 挂接本地地址
接着测试是否挂接->取消挂接->删除shm的过程是否成功:
server.cc和clent中都遵循:创建/获取shmid,挂接,解除挂接,删除的规则,当然删除只需要创建的一方就可以了,我们这里选择server。在此之前被忘了对sharememory进行初始化对象。
server.cc / clent.cc /测试
接下来测试:
我们还可以进一步的进行改进,可以注意到一些如k等的值是可以放在类里面进行管理的,这样server和clent都可以直接看到并且都有一份。
我们仅仅放置 _shmid(共享内存id),key(k值),_addr(映射地址)作为私有。如下修改减少了return的必要。
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <sys/stat.h>
using namespace std;
const string path = "/home/yulin2/linuxtest";
int projid = 0x6666;
int gshmsize = 4098;
mode_t gmode = 0600;
class sharememory
{
private:
int creatshmhelper(int shmflg)
{
//umask(0);
_key = ::ftok(path.c_str(), projid);
if (_key < 0)
{
cerr << "ftok fail!" << endl;
return -1;
}
cout << "k: " << _key << endl;
// 创建共享内存
_shmid = ::shmget(_key, gshmsize, shmflg);
if (_shmid < 0)
{
cerr << "shmget error" << endl;
return -2;
}
cout << "shmid: " << _shmid << endl;
return _shmid;
}
public:
sharememory()
:_shmid(-1)
,_key(-1)
,_addr(nullptr)
{}
~sharememory()
{}
// 创建shm
void creatshm()
{
creatshmhelper(IPC_CREAT | IPC_EXCL | gmode);
}
// 得到shm,已有的获取不创建
void getshm()
{
creatshmhelper(IPC_CREAT | gmode);
}
// 挂接本地地址
void attachshm()
{
_addr = shmat(_shmid, nullptr, 0);
if ((long long)_addr == -1)
{
cout << "attach error" << endl;
}
cout << "attach done" << (long long)_addr << endl;
}
// 解除映射
void detachshm()
{
if (_addr != nullptr)
::shmdt( _addr);
cout << "detach done: " << (long long) _addr << endl;
}
// 删除shm
void deleteshm()
{
shmctl(_shmid, IPC_RMID, nullptr);
}
//打印属性
void shmmeta()
{}
private:
int _shmid;
key_t _key;
void *_addr;
};
sharememory shm;
这个打印属性的函数在当前没有实现的必要。对应的.cc也要进行修改:
我就不监视了,毕竟肯定会看到nattch等于2的。
经过我们如上一堆骚操作,只是将框架搭好了,我们还没有开始通信呢,我们现在就开始:
IPC通信
我们直接选择在.cc中进行通信,我们首先要拿得到挂接的虚拟地址,所以我们添加函数getaddr,我们直接向这个地址内写入就可以了,因为这个地址相当于共享内存,因为我们挂接了的。字符串名字就是这个首地址,所以这个地址我们可以理解为字符串,当然我们也可以把它当成结构体等等。C++打印char* 相当于打印这个指针指向的字符串。
我们直接在server里面打印这个字符串(那个指针),然后在clent里面写入这个空间,看结果。
#include"sharememory.hpp"
int main()
{
shm.creatshm();
shm.attachshm();
//sleep(10);
cout << "server attach done" << endl;
//进行IPC
char* strinfo = (char*)shm.getaddr();
while(true)
{
printf("%s\n", strinfo);
sleep(1);
}
shm.detachshm();
cout << "server detach done" << endl;
//sleep(10);
shm.deleteshm();
cout << "detel shm" << endl;
return 0;
}
#include"sharememory.hpp"
int main()
{
shm.getshm();
shm.attachshm();
//sleep(10);
cout << "clent attach done" << endl;
char* strinfo = (char*)shm.getaddr();
char ch = 'A';
while(ch <= 'Z')
{
sleep(3);
strinfo[ch - 'A'] = ch;
ch++;
}
shm.detachshm();
cout << "clent detach done" << endl;
return 0;
}
我们可以看到当我们只打开读端时,server并没有像管道那样没有东西可读就停止,而是直接读空,当clent以三秒一次写入时,读端确实读了,但是没有去重呀,可见共享内存的读取是不等写进程了自我独立运行的,是让两个进程各自共享用户空间内存块,当我们关闭读端时,写端还在写入,反之也是类似的,所以没有加任何保护机制,这个很危险的,这种让进程具有独立性的方法,容易让两进程数据不一致(一端还在写,一端已经读取了),我们就需要对共享内存自己完成保护,我们一般使用加锁,或者使用数据量保护处于临界区的临界资源。基于管道的性质,我们还可以使用命名管道进行保护!!!但是这种方法也是有风险的,所以加锁最好!!!
共享内存不需要系统级的文件操作而是直接写入,所以这就意味着写入共享内存不存在文件缓冲区,所以通信速度最快。进程是直接看到映射的共享内存,所以不需要文件操作。
使用命名管道保护
写端在循环写入共享内存的同时先写入命名管道,等单次写入命名管道完成时,再提醒读端读取同时读取共享内存的内容,所以读端先访问管道,这是单向的,如果要完成双向的通信就需要两个命名管道。
命名管道在此的作用就是提醒读取的作用,等单次写入完成了再读,没有数据就不要读取,但是这样还是没有避免重复读取,只是避免了读取写入不一致,本质还是要到共享内存读的。命名管道(FIFO)在这里的作用更像是一个通知机制,用于同步写入和读取的时机,而真正的数据还是存放在共享内存(Shared Memory, SHM)中。解决共享内存的数据一致性问题,还是必须考虑加锁。如果多个进程可能同时访问共享内存,就需要同步机制来确保数据的正确性。常见的同步手段包括互斥锁(mutex)、读写锁、信号量等。