目录
在之前,无论是匿名管道还是命名管道,说到底都是基于文件的通信,也就意味着没有为了通信让OS单独设计一套通信模块代码,而是直接复用内核中文件相关的数据结构、缓冲区、代码来实现通信的,这在一定程度上是设计者偷懒,但是也符合我们软件工程尽可能复用代码的思想。除了文件版的通信外,还有人专门为通信从零设计了一套方案,也就是本地通信方案的代码:System V IPC,这套方案有几个典型代表 :共享内存、消息队列、信号量,但是这种方案由于很多原因已经处于淘汰的边缘了,我们重点需要了解共享内存。
原理
首先,由OS在物理内存为我们①开辟一段内存空间,然后②内存空间通过页表映射到地址空间中的共享区,于是,这段共享区和内存空间构成映射,然后,将共享内存的起始虚拟地址返回给上层用户,用户就可以通过虚拟地址经页表转向共享内存中写内容了,那另一个进程也可以映射这段内存空间。我们把这种用地址空间映射进而能让两个进程看到同一块内存空间的过程叫做共享内存。
对于上面的原理,我们有几点理解:
- 上面所说的操作,都是OS做的。
- OS提供上面的①②步骤的系统调用,供用户进程A、B来进行调用。
- 共享内存在系统中可以是存在多份的,供不同对进程同时进行通信。
- OS注定要对共享内存进行管理,先描述,在组织。共享内存不是简单的一段内存空间,也要有描述并管理共享内存的数据结构和匹配的算法。
我们通过 shmget 系统调用创建共享内存,我们来查看一下这个接口的说明:
其中,第二个参数size表示共享内存shm创建多大。第三个参数shmflg是标志位,通过位图的方式来传参,主要选项是IPC_CREAT和IPC_EXCL。IPC_CREAT:如果要创建的共享内存不存在,创建之,如果存在,获取该共享内存并返回,意味着总能获取一个shm;IPC_EXCL:单独使用无意义,只有和IPC_CREAT组合才有意义;IPC_CREAT|IPC_EXCL:如果要创建的共享内存不存在,创建之,如果存在,出错返回 ---- 这个选项组合的意义在于,如果成功返回了,就意味这个shm是全新的。
函数的返回值是共享内存的id。
上面说了这么多,有一个问题是,我(进程)怎么知道OS内共享内存是否存在呢?我们已经知道,共享内存是要有描述自己的数据结构的,而我们必须区分清楚某个共享内存是不是存在,那么我们可以大胆预言一下,共享内存的属性里一定要有标识共享内存唯一性的字段!!所以,我们可以通过共享内存的唯一性标识符判断是不是存在。
为了使另一个进程找到已经创建的共享内存,为此shmget的第一个参数key是由用户形成的,key是多少不重要,只要有唯一性即可!可以通过key来判断这个共享内存是不是存在。
实际上,OS提供ftok用于生成key。
有了这个调用,这意味我们可以给AB通信的两个进程,使用同样的pathname,同样的proj_id,调用同样的ftok,我们就能形成同样的key了,然后,一个进程创建shm,另一个进程获取shm,就看看到同一个共享内存了。
代码
我们先需要让两个进程得到同一个key,于是我们设计了GetCommonKey函数:
const std::string gpathname = "/home/ghs/linux/linux/lesson22/4.shm";
const int gproj_id = 0x66;
key_t GetCommKey(const std::string& pathname,int proj_id)
{
key_t k = ftok(pathname.c_str(),proj_id);
if(k < 0)
{
perror("ftok");
}
return k;
}
server和client两个进程都调用这个函数,就得到了相同的key:
我们在运行代码的时候,发现,共享内存不随着进程的结束而自动释放!因为共享内存不属于进程,而属于OS。共享内存会一直存在,直到系统重启。需要我们手动释放(指令/其他系统调用)。也就是共享内存的生命周期随内核,文件的生命周期随进程!
我们可以使用指令 ipcs -m 指令查共享内存,
嘿嘿,查到了之前创建的共享内存,然后可以按照 ipcrm -m shmid删除共享内存(而不是按照key删):
到现在,我们可以对比一下 key 和 shmid,
key:属于用户形成,内核使用的一个字段,用户不能使用key来进行shm的管理。内核进行区分shm唯一性的。(类似struct file*)
shmid:内核给用户返回的一个标识符,用来进行用户级对shm进行管理的id值。(类似fd)
我们创建一个类Shm,
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;
}
public:
Shm(const std::string &pathname, int proj_id, int who)
: _pathname(pathname), _proj_id(proj_id), _who(who)
{
_key = GetCommKey();
if(_who == gCreater) GetShmUseCreate();
else if(_who == gUser) GetShmForUse();
std::cout << "shmid: " << _shmid << std::endl;
std::cout << "key: " << ToHex(_key) << std::endl;
}
~Shm() {}
std::string ToHex(key_t key)
{
char buffer[128];
snprintf(buffer, sizeof(buffer), "0x%x", key);
return buffer;
}
bool GetShmUseCreate()
{
if (_who == gCreater)
{
_shmid = GetShmHelper(_key, gShmSize, IPC_CREAT | IPC_EXCL);
if (_shmid >= 0)
return true;
}
return false;
}
bool GetShmForUse()
{
if (_who == gUser)
{
_shmid = GetShmHelper(_key, gShmSize, IPC_CREAT);
if (_shmid >= 0)
return true;
}
return false;
}
private:
key_t _key;
int _shmid;
std::string _pathname;
int _proj_id;
int _who;
};
server和client分别创建一个shm对象,
它们找到了同一块共享内存。
那么,我们怎么删除共享内存呢?其实,我们可以使用 shmctl 接口调用,
既然叫*ctl,那么它不仅可以删除shm,还可以做其他的操作(比如获取属性),cmd的选项为IPC_RMID时可以删除shm,因此,我们在析构函数中加入删除shm的操作。
现在,我们已经可以创建好共享内存,接下来就是让共享内存通过页表映射到地址空间(挂接到进程),那共享内存该如何挂接呢?有一个系统调用接口 shma(at-attach) ,还有一个调用时shmdt(dt-detach,去关联):
shmat:其中,shmid就是我们自己申请的共享内存。shmaddr就是我们想挂接到哪个地址上,但是现在我们不考虑,设为null。shmflg就是设置共享内存的访问权限,在这里我们设置为读写,设为0即可。那么其返回值void*是什么呢?其实是地址空间中共享内存的起始地址,这点和malloc很相似,malloc返回堆上开辟空间的起始地址。
shmdt:去挂接,shmaddr为shmat的返回值。
接下来我们开始挂接共享内存:
void* AttachShm()
{
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* addr)
{
if(addr == nullptr) return;
shmdt(addr);
std::cout << "who: " << RoleToString(_who) << " detach shm..." << std::endl;
}
可以看到,server和client依次运行,先挂接后去挂接,共享内存的挂接数由0->1->2->1->0。
到此,别看我们做了很多,但是我们还没有开始通信!!根本原因在于我们是进程间通信,我们之前的工作只能叫通信的准备工作。
现在我们开始通信:
我们的server.cc是:
int main()
{
Shm shm(gpathname,gproj_id,gCreater);
char* addr = (char*)shm.Addr();
while(true)
{
std::cout << "shm memory content: " << addr << std::endl;
sleep(1);
}
return 0;
}
我们的client.cc是:
int main()
{
Shm shm(gpathname,gproj_id,gUser);
shm.Zero();
char* addr = (char*)shm.Addr();
sleep(3);
//当成string
char ch = 'A';
while(ch <= 'Z')
{
addr[ch - 'A'] = ch;
ch++;
sleep(2);
}
return 0;
}
我们先运行server,发现server一直在读,
并没有阻塞在这里,然后,运行client,写数据,
这里两秒读一次,我们很奇怪的发现,第一次读完之后,第二次还可以读到一样的数据,这是因为共享内存不提供对共享内存的任何保护机制,这样可能导致我还没写完就被读走了,会导致数据不一致问题。其次,我们在访问共享内存的时候,没有用任何系统调用,所以,共享内存是所有进程IPC,速度最快的,因为,共享内存大大减少了数据内存的拷贝次数。那么,怎么对共享内存施加保护呢?当共享内存中没有数据时就不要读,等有数据的时候并且我想让你读的时候你再读!我们之前已经知道,管道是存在同步机制的,所以,server和client除了建立共享内存外,还要建立管道,server在从共享内存中读数据前,先去读管道,管道没数据的话,server就等着。当client写完数据后,通过管道通知server再来读,这不就完成了对共享内存的保护了吗?
在上面,我们设置了共享内存的大小是4096,我们建议其大小是4096*n,
我们如何获取共享内存的属性呢?需要使用shmctl调用,
shmctl的第二个参数我们设置成IPC_STAT,就可以获取共享内存的属性信息了。
void DebugShm()
{
struct shmid_ds ds;
int n = shmctl(_shmid, IPC_STAT, &ds);
if(n < 0) return;
std::cout << "ds.shm_perm.__key :" << ToHex(ds.shm_perm.__key) << std::endl;
std::cout << "ds.shm_nattch"<< ds.shm_nattch << std::endl;
}
关于共享内存,这里有两点需要着重强调:
- 共享内存的key是在用户层面生成的,需要两个进程使用同一个算法生成相应的key。
- 两个进程,一个用来创建,一个用来获取,这和命名管道类似。
完整代码见下面:
Shm.hpp
#ifndef __SHM_HPP__
#define __SHM_HPP__
#include <iostream>
#include <cstdio>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
const int gCreater = 1;
const int gUser = 2;
const std::string gpathname = "/home/ghs/linux/linux/lesson22/4.shm";
const int gproj_id = 0x66;
const int gShmSize = 4096;
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 == gCreater)
return "Creater";
else if (who == gUser)
return "User";
else
return "None";
}
void *AttachShm()
{
if (_shmaddr != nullptr)
DetachShm(_shmaddr);
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 *addr)
{
if (addr == nullptr)
return;
shmdt(addr);
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), _shmaddr(nullptr)
{
_key = GetCommKey();
if (_who == gCreater)
GetShmUseCreate();
else if (_who == gUser)
GetShmForUse();
_shmaddr = AttachShm();
std::cout << "shmid: " << _shmid << std::endl;
std::cout << "key: " << ToHex(_key) << std::endl;
}
~Shm()
{
if (_who == gCreater)
{
int res = shmctl(_shmid, IPC_RMID, nullptr);
}
std::cout << "shm remove done..." << std::endl;
}
std::string ToHex(key_t key)
{
char buffer[128];
snprintf(buffer, sizeof(buffer), "0x%x", key);
return buffer;
}
bool GetShmUseCreate()
{
if (_who == gCreater)
{
_shmid = GetShmHelper(_key, gShmSize, IPC_CREAT | IPC_EXCL | 0666);
if (_shmid >= 0)
{
std::cout << "shm create done..." << std::endl;
return true;
}
}
return false;
}
bool GetShmForUse()
{
if (_who == gUser)
{
_shmid = GetShmHelper(_key, gShmSize, IPC_CREAT | 0666);
if (_shmid >= 0)
{
std::cout << "shm use done..." << std::endl;
return true;
}
}
return false;
}
void *Addr()
{
return _shmaddr;
}
void Zero()
{
if(_shmaddr)
{
memset(_shmaddr,0,gShmSize);
}
}
void DebugShm()
{
struct shmid_ds ds;
int n = shmctl(_shmid, IPC_STAT, &ds);
if(n < 0) return;
std::cout << "ds.shm_perm.__key :" << ToHex(ds.shm_perm.__key) << std::endl;
std::cout << "ds.shm_nattch"<< ds.shm_nattch << std::endl;
}
private:
key_t _key;
int _shmid;
std::string _pathname;
int _proj_id;
int _who;
void *_shmaddr;
};
#endif
namedPipe.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdio>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define Creater 1
#define User 2
#define BaseSize 4096
#define DefaultFd -1
#define Read O_RDONLY
#define Write O_WRONLY
const std::string comm_path = "./myfifo";
class NamedPipe
{
private:
bool OpenNamedPipe(int mode)
{
_fd = open(_fifo_path.c_str(), mode);
if (_fd < 0)
return false;
return true;
}
public:
NamedPipe(const std::string &path, int who)
: _fifo_path(path), _id(who), _fd(DefaultFd)
{
if (who == Creater)
{
int res = mkfifo(path.c_str(), 0666);
if (res < 0)
{
perror("mkfifo");
}
std::cout << "creater create named pipe" << std::endl;
}
}
bool OpenForRead()
{
return OpenNamedPipe(Read);
}
bool OpenForWrite()
{
return OpenNamedPipe(Write);
}
int ReadNamedPipe(std::string* out)
{
char buffer[BaseSize];
int n = read(_fd, buffer, sizeof(buffer));
if(n > 0)
{
buffer[n] = 0;
*out = buffer;
}
return n;
}
int WriteNamedPipe(const std::string& in)
{
return write(_fd, in.c_str(), in.size());
}
~NamedPipe()
{
if (_id == Creater)
{
//sleep(5);
int res = unlink(_fifo_path.c_str());
if (res < 0)
{
perror("unlink");
}
std::cout << "creater free named pipe" << std::endl;
}
if(_fd != DefaultFd) close(_fd);
}
private:
const std::string _fifo_path;
int _id;
int _fd;
};
server.cc
#include "Shm.hpp"
#include "namedPipe.hpp"
int main()
{
//1.创建共享内存
Shm shm(gpathname,gproj_id,gCreater);
char* addr = (char*)shm.Addr();
shm.DebugShm();
// //2.创建管道
// NamedPipe fifo(comm_path, Creater);
// fifo.OpenForRead();
// while(true)
// {
// std::string temp;
// fifo.ReadNamedPipe(&temp);
// std::cout << "shm memory content: " << addr << std::endl;
// }
return 0;
}
client.cc
#include "Shm.hpp"
#include "namedPipe.hpp"
int main()
{
//1.创建共享内存
Shm shm(gpathname,gproj_id,gUser);
shm.Zero();
char* addr = (char*)shm.Addr();
sleep(3);
//2.打开管道
NamedPipe fifo(comm_path, User);
fifo.OpenForWrite();
//当成string
char ch = 'A';
while(ch <= 'Z')
{
addr[ch - 'A'] = ch;
std::string temp = "wakeup";
std::cout << "add " << ch << " into Shm, " << "wake up reader" << std::endl;
fifo.WriteNamedPipe(temp);
ch++;
sleep(2);
}
return 0;
}