目录
理解共享内存
理解共享内存
前文介绍的管道方式的通信,本文介绍的是进程通信的另外一种方式,即共享内存。但是这种通信方式的特点是只能本地通信,并且不像管道那样有保护机制,这里是没有的。
我们通过这个图,引出我们今日的话题:
在Linux中,万物皆是文件的概念已经深深的刻入到了我们的大脑里面,在文件系统里面我们介绍了进程,介绍了地址空间,介绍了页表,介绍了物理内存之间的映射关系,知道了代码和数据的地址通过页表,将虚拟地址和物理地址完美联系在了一起,那么物理内存里面是否存在进程间通信需要的空间 -- 共享内存呢?
当然是存在的,其实在动静态库的部分,我们就知道了动态库就是将库的内容加载到了物理内存上,不同间的进程通过页表可以找到对于的库的内容,这在博主看来其实是一种共享内存,可是,共享内存的开辟由谁来做?怎么知道共享内存开辟的空间的地址?
上面两个问题对应的操作其实都是由OS来完成的,但是OS是肯定不能自己来完成的,因为OS是要根据用户的需求实施对应的操作,所以这两个操作,OS给我们提供了系统调用,由我们用户来执行即可。
那么新的问题来了:是否存在多个共享内存?如果存在多个共享内存,那么OS是否有必要对共享内存进行管理?如果要实施管理,OS是如何进行管理的?
对于第一个问题,答案是肯定的,因为不只是有AB两个进行需要使用共享内存进行通信,还有CD,还有EF需要使用共享内存进行通信。
对于第二个问题,OS肯定是有必要对共享内存进行管理的,不然内存导致的问题由谁来负责呢?
对于第三个问题,我们直接call back前面的文件部分了,想要对某种对象进行管理,那么使用到的一定是六字真言,先描述,再组织!!!
在Linux源码里面是有共享内存对应的结构体的,这里因为不介绍,所以不放出对应的源码了,肯定就有人说了,怎么又又又是结构体?因为Linux就是C语言写的呀,并且,C语言想要对某个对象管理,结构体不是最好的选择吗?
所以我们得出一个结论,共享内存 = 共享内存的数据 + 共享内存的属性!!
那么我们现在就可以直接进入到了代码部分了。
Shared memmory code
对于共享内存的代码,我们使用的是和命名管道一样的方式,一个客户端,一个服务端,一个hpp文件,我们首先最关心的,就是如何创建共享内存?
也就是第一个问题,使用的系统调用是2号手册的shmget:
对于头文件部分不用解释,对于三个参数部分,一个是key_t类型的key,一个是size,一个是shmflg。
size代表的是开辟的共享内存的大小,对于shmflg,也就是共享内存的标志,我们这里就介绍两个常用的,一个是IPC_CREAT 一个是IPC_EXCL,使用时候我们可以分为IPC_CREAT使用,IPC_EXCL单独使用没有意义,IPC_CREAT | IPC_EXCL使用。
对于第一种模式,IPC_CREAT,代表的是如果创建的共享内存不存在,就创建,如果存在共享内存,就获取该共享内存并返回,说白了就是总能够获取一个共享内存,但是不一定是全新的。
对于第二种模式,IPC_CREAT | IPC_EXCL,代表的是如果创建的共享内存不存在,就创建,如果存在了对应的共享内存,就出错返回,也就是说,这个模式获取到的共享内存一定是全新的。
最后一个参数,key,我们首先思考一个问题,开辟了共享内存之后,进程通过什么方式知道共享内存呢?难道是A进程开辟了这个共享内存,然后打电话给B进程说:喂,我开辟了一个共享内存,地址是0x34381fec。这样肯定是不可以了,因为我们探究的就是进程通信,这还没有通信呢,怎么让他们告知对方呢?
所以获取共享内存标识符的方法是不能让进程生成的,肯定是要让用户自己形成的,所以需要介绍到一个函数为ftok:
我们需要给一串路径,一个id,那么在ftok内部,就可以通过某种算法,实现key的生成。
那么对于函数shmget的返回值的描述是:
返回的值如果成功了,返回的是共享内存的唯一标识符,如果开辟共享内存失败了,返回的就是就是-1。
话不多说,我们先创建一个,并且打印出来看看:
const char *pathname = "/home/lazy/linux/lower_code/shm";
const int proj_id = 0x11;
std::string ToHex(key_t key)
{
char buffer[128];
snprintf(buffer, sizeof(buffer), "0x%x", key);
return buffer;
}
int main()
{
key_t key = ftok(pathname, proj_id);
int shmid = shmget(key, 4096, IPC_CREAT | IPC_EXCL);
std::cout << "key: " << ToHex(key) << " shmid " << shmid << std::endl;
return 0;
}
转换为16进制是为了后面方便观察,我们使用混合模式创建了共享内存:
最开始使用宏只有IPC_CREAT,后面使用了IPC_EXCL,我们会发现前面创建的共享内存还是存在,所以会报错,可是,明明我们的进程已经结束了,为什么共享内存还在呢!!
所以,我们得出一个结论,共享内存的生命周期不随进程终止而终止。那么后面就势必会牵扯到共享内存的回收问题。
我们通过代码系统调用的方式,已经能成功创建了,但是我们想拿出来看看怎么办,我们使用命令行ipcs -m就可以进行查看相关信息了:
其中key是16进程的,所以我们前面会转成16进程的方便观察,shmid是0,owner是lazy,perms权限为0,共享内存的大小是4096,nattch对应的是0,代表的意思是挂接的进程为0,status状态。
那么我们想要删除,使用的命令是ipcrm,这里提问了就,我们使用key删除还是shmid进行删除呢?
当然是shmid了,对于key不过是共享内存的一个标识符,告诉OS可以通过key来找到对应的共享内存,对于shmid,是可以实现用户级别进行管理的一个值,所以我们作为用户,肯定是通过shmid进行管理的:
这样就行了。
说了那么多,我们对共享内存的函数也了解了,我们也是时候应该对它进行一些封装了。
因为我们是用C++语言实现的,所以仍然使用类的方式进行实现:
#ifndef __SHM_HPP__
#define __SHM_HPP__
#include <iostream>
#include <string>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
const char* pathname = "/home/lazy/linux/lower_code/shm";
const size_t shm_size = 4096;
const int proc_id = 0x66;
class shm
{
public:
private:
key_t _key;
int _shmid;
std::string _pathname;
int proc_id;
};
#endif
先将基本的框架搭建好。
然后首先构造函数部分,因为key _shmid都是用户层面自己提供的,所以我们在构造函数提供。
并且考虑到分为了服务端和用户端,我们就可以新增一个_who,用来表明身份。
shm(const std::string& pathname, int proc_id, int who)
:_pathname(pathname),_proc_id(proc_id),_who(who)
{
}
那么获取到id,我们如果在构造函数里面直接写就有点不美观了,我们可以单独封装一个函数出来:
key_t Getkey()
{
key_t key = ftok(_pathname.c_str(), _proc_id);
if (key < 0)
{
perror("ftok");
}
return key;
}
并且因为这个函数是用来获取key的,用户不应该直接调用,所以为了增加用户的体验,我们应该将这种类型的函数设置为私有的。
// 获取shmid
int GetShmid(key_t key, size_t size, int shmflg)
{
int shmid = shmget(key, size, shmflg);
if (shmid < 0)
{
perror("shmget");
}
return shmid;
}
// Creater的封装
bool GetUserForCreate()
{
if (_who == Creater)
{
_shmid = GetShmid(_key, shm_size, IPC_CREAT | IPC_EXCL | 0666);
if (_shmid > 0)
return true;
}
std::cout << "shm fail create..." << std::endl;
return false;
}
对于Creater获取shmid,我们仍然是在构造函数里面实现,并且,简单的通过两层封装实现,在IPC_EXCL后面的0666本质上是permission,这里暂时先不用管。
那么对于Creater的函数到这里了,对于user来说,构造函数还没有实现,我们要清楚user使用该类的时候要干什么,好吧,其实也没有什么特别要干的,只是它需要知道shmid罢了。
// user的封装
bool GetUserForUser()
{
if (_who == User)
{
_shmid = GetShmid(_key, shm_size, IPC_CREAT | IPC_EXCL | 0666);
if (_shmid > 0)
return true;
}
std::cout << "shm fail create..." << std::endl;
return false;
}
就像这样。
较为完整的构造函数就是:
Shm(const std::string &pathname, int proc_id, int who)
: _pathname(pathname), _proc_id(proc_id), _who(who)
{
_key = Getkey();
if (_who == Creater)
GetUserForCreate();
else
GetUserForUser();
}
那么我们不妨使用server来试试?
#include "shm.hpp"
#include <iostream>
int main()
{
//创建共享内存
Shm shm(pathname,proc_id,Creater);
return 0;
}
那么实验成功了,但是,这里提问:
到现在位置,进程这里是否开始通信呢?
答案是:没有!!!
因为进程之间使用共享内存是要进行挂接的,也就是将共享内存的地址給进程。
那么我们得知道地址吧?
shmid
:这是由shmget函数返回的共享内存对象的系统标识符。shmaddr
:这是一个可选参数,用于指定共享内存区域在进程的虚拟地址空间中的起始地址。如果设置为NULL,则由系统选择地址。shmflg
:这是一个标志参数,用于控制连接的行为。例如,它可以指定是否允许共享内存区域在调用进程的地址空间中固定位置,或者是否允许读写访问等
那么为了获得地址,我们在类的私有成员变量里面新增一个_addrshm。
// 获取共享内存的地址
void *AttachShm()
{
void *shmaddr = shmat(_shmid, nullptr, 0);
if (shmaddr == nullptr)
{
perror("shmat");
}
return shmaddr;
}
并且在构造函数里面使用一个函数用来初始化_addrshm。
可是当我们不再想使用该内存了,我们就可以使用函数shmdt,将该共享内存空间分离出去,也就是当_addrshm不为空的时候:
// 获取共享内存的地址
void *AttachShm()
{
if (_addrshm != nullptr)
DetachShm(_addrshm);
void *shmaddr = shmat(_shmid, nullptr, 0);
if (shmaddr == nullptr)
{
perror("shmat");
}
return shmaddr;
}
void DetachShm(void *shmaddr)
{
if(shmaddr == nullptr)
return;
shmdt(shmaddr);
}
这样,地址我们就知道了,可是仍然没有挂接上,挂接使用的函数是shmctl:
其中也有共享内存的结构体信息:
有了地址,就可以通信了。
那么通信只需要server和client端口都获取到共享内存的地址就可以了:
#include "shm.hpp"
int main()
{
//创建共享内存
Shm shm(pathname,proc_id,creater);
char* shmaddr = (char*)shm.Addr();
while(true)
{
std::cout << "shm memory content: " << shmaddr << std::endl;
sleep(1);
}
return 0;
}
#include "shm.hpp"
int main()
{
Shm shm(pathname, proc_id, user);
shm.Zero();
char *shmaddr = (char *)shm.Addr();
char ch = 'A';
while (ch <= 'Z')
{
shmaddr[ch - 'A'] = ch;
std::cout << "client write " << ch << std::endl;
sleep(2);
ch++;
}
return 0;
}
主要操作是shmaddr获取到地址,获取到了地址就可以了,那么为了方便观察,我们使用sleep函数休眠上一秒两秒。
现象就是:
它都不带有任何保护机制的,所以server端是在一直读取,这也就是为什么快了,它不像管道那样约束很多,所以我们可以在共享内存里面引入管道,也就是增加管道机制即可。
具体实现交给大家了~
感谢阅读!