个人主页:Lei宝啊
愿所有美好如期而遇
我们知道,进程是具有独立性的,什么叫进程具有独立性?就是说,每一个进程都有他们自己的虚拟地址空间,而虚拟进程空间里的虚拟地址通过页表映射到不同的物理内存上,进程独立性很大一部分是因为这个原因。
同时我们需要提到的一点是,即使是虚拟地址空间通过页表映射的物理内存,这个物理内存是不属于进程的,他仍然是属于操作系统的,只是进程可以访问这块空间。
所以假设我们有两个进程,进程A和进程B,由进程的独立性也就决定了进程A不能访问进程B的数据,但我们仍然希望可以有一块公共的空间可以使得他们之间可以交换数据,这就是共享内存,由操作系统开辟,通过页表映射到虚拟地址空间的共享区,并且共享内存比较特殊的一点是,他不会破坏进程的独立性,你可能有这样的疑问,既然进程的独立性很大一部分是由进程的虚拟地址空间所决定的,那么两个进程的虚拟地址空间通过页表映射到了同一块物理内存,并且可以互相访问,这难道不是破坏了进程的独立性吗?
并不是这样的,虽然多个进程可以访问同一块物理内存,但它们仍然是通过各自的虚拟地址空间进行访问的,每个进程都有自己的页表,将其虚拟地址映射到共享内存的物理地址。这样,每个进程仍然保持其地址空间的独立性,只是在共享内存区域上有所重叠。
接下来,我们将使用共享内存来实现进程间通信。
首先我们需要创建共享内存,但是涉及到内存的开辟,这种事情只有操作系统才有权限,我们想要做到,就只有使用操作系统提供的系统调用。
创建共享内存---shmget
int shmget(key_t key, size_t size, int shmflg);
第一个参数---key
既然共享内存可以使进程间通信,那么内存中就可能不止一个共享内存,所以我们需要一个唯一标识共享内存的值,这个key值就类似于文件描述符中的struct file*,他唯一确定一个共享内存。
但是这个值不由操作系统去生成,而是由我们用户通过ftok函数,也就是一个算法计算得来,那么为什么操作系统不去生成一个key,而是非要我们用户去传呢?
首先我们要明白,进程间通信,如果一个进程中的共享内存的key值是由操作系统所决定的,那么另一个想和他通信的进程,如何在内存中那么多的共享内存中找到要通信的进程所创建的共享内存?答案显而易见是不可能的,操作系统他没有办法将这个key传给另一个进程,如果可以将这个key传给另一个进程,那么你都能通信了,我们还要共享内存干什么?所以这个key值只能是由将要进行通信的两个进程共同决定后计算得出。
key_t ftok(const char *pathname, int proj_id);
第二个参数---size
这个参数将决定创建的共享内存的大小,单位为字节,但是我们需要注意的是,共享内存开辟时,将会以4096字节的倍数开辟,如果说我们想要开辟4097个字节的共享内存,那么他会开辟8192字节的内存空间,但是我们只能使用4097,这就类似于reserve和resize。
第三个参数---shmflg
这是两个宏,第一个宏的意思是,创建一个共享内存;如果这个共享内存已经存在,则返回这块空间的标识符。第二个宏不可以单独使用,需要配合第一个宏使用,两者或以后,意为:创建一个共享内存;如果这个共享内存已经存在,则shmget调用失败,返回-1,设置错误码。
返回值
这个函数的返回值是shmid,也就是共享内存标识符,他其实类似于fd。
连接共享内存---shmat
void *shmat(int shmid, const void *shmaddr, int shmflg);
第一个参数传shmget返回的shmid,第二个参数指定共享内存在进程地址空间中的连接位置,我们默认为nullptr时,操作系统会自动选择一个合适的地址去连接共享内存。第三个参数指定了共享内存的读写权限,我们默认0就可以进行读写。
返回值
返回一个指向共享内存的指针(共享内存在进程虚拟地址空间上的连接位置,即虚拟地址)
取消连接共享内存---shmdt
int shmdt(const void *shmaddr);
这个参数我们传的就是shmat的返回值,也就是传共享内存的地址(也就是共享内存在进程虚拟地址空间中的连接位置)
控制共享内存(删除,获取,设置)---shmctl
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
这个函数返回值对于不同的cmd参数有不同,但对于我们将要介绍的cmd参数来说是一样的,就是这样:
删除
设置cmd参数为IPC_RMID,第三个参数我们稍后会介绍,这里的删除操作对他设置为空即可。
获取共享内存属性
所以我们第三个参数是用来获取共享内存属性的,需要配合IPC_STAT这个cmd参数。
实现进程间通信
我们希望两个没有亲缘关系的进程进行通信,所以写一个服务端·,一个客户端。
我们先来看服务端的主逻辑:
我们封装了一个shm类管理共享内存,这个类将实现共享内存的创建,连接,去关联,获取信息,关闭操作,因为每一个操作都需要shmget返回的shmid,所以我们设置一个shmid成员变量,以及连接后返回共享内存地址,我们也需要保存,因为后面还需要去关联,所以我们再设置一个addr成员变量。
reader操作,读取客户端发送的消息。
int main()
{
shm shared_memory;
shared_memory.create_shm();
shared_memory.shm_attach();
reader((char*)shared_memory.get_addr());
shared_memory.shm_detach();
shared_memory.close_shm();
return 0;
}
template<class T>
void reader(T* addr)
{
SyncLast sync;
sync.CreatePipe();
sync.ROpen();
for(;;)
{
if(!sync.Wait()) break;
cout << addr << endl;
}
}
接下来是客户端的主逻辑:
这里我们不需要创建共享内存,只需要获得共享内存的shmid即可,但是我们不能直接get_shmid,因为这是客户端,是另一个进程,所以我们只需要IPC_CREAT获取shmid即可。
在客户端,我们不需要释放共享内存,去关联即可。
writer即向服务端发送消息。
int main()
{
shm shared_memory;
shared_memory.get_shm();
shared_memory.shm_attach();
writer((char*)shared_memory.get_addr());
shared_memory.shm_detach();
return 0;
}
template<class T>
void writer(T* addr)
{
SyncLast sync;
sync.WOpen();
memset(addr,0,SIZE);
for(int i='A'; i<='Z'; i++)
{
addr[i-'A'] = i;
sync.Wake();
sleep(1);
}
}
在这里要先提到一件事情,共享内存没有同步机制,也就是说,如果我们将服务端跑起来,那么服务端会一直读取消息,即使客户端没有发送,而这点是我们不想看到的,所以我们可以使用信号量来进行实现,博主这里就使用管道简单的实现一下同步就可以了。
接下来我们看实现。
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <cerrno>
#include <cstring>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;
#define PATH "/home"
#define PROJ_ID 66
#define SIZE 4096
class shm
{
public:
shm()
:_shmid(-1)
,_addr(nullptr)
{}
key_t getkey(const char* path = PATH, int proj_id = PROJ_ID)
{
key_t key = ftok(path, proj_id);
if(key < 0)
{
cout << "ftok error:" << errno << " reason:" << strerror(errno)
<< endl;
exit(1);
}
cout << "getkey success:" << key << endl;
return key;
}
void create_shm(int size = SIZE, const char* path = PATH,
int proj_id = PROJ_ID)
{
_shmid = shmget(getkey(path,proj_id),size,IPC_CREAT | IPC_EXCL);
if(_shmid < 0)
{
cout << "shmget error:" << errno << " reason:" << strerror(errno)
<< endl;
exit(2);
}
cout << "create_shm success" << endl;
}
int get_shm(int size = SIZE, const char* path = PATH, int proj_id = PROJ_ID)
{
_shmid = shmget(getkey(path,proj_id),size,IPC_CREAT);
if(_shmid < 0)
{
cout << "get_shm error:" << errno << " reason:" << strerror(errno)
<< endl;
exit(3);
}
cout << "get_shm success" << endl;
return _shmid;
}
bool shm_attach()
{
_addr = shmat(_shmid,nullptr,0);
if(_addr == (void*)-1)
{
cout << "shmat error:" << errno << " reason:" << strerror(errno)
<< endl;
return false;
}
cout << "shm_attach success" << endl;
return true;
}
bool shm_detach()
{
int shmdt_return_val = shmdt(_addr);
if(shmdt_return_val == -1)
{
cout << "shmdt error:" << errno << " reason:" << strerror(errno)
<< endl;
return false;
}
cout << "shm_detach success" << endl;
return true;
}
void close_shm()
{
int shmctl_return_val = shmctl(_shmid,IPC_RMID,nullptr);
if(shmctl_return_val == -1)
{
cout << "close_shm error:" << errno << " reason:" <<strerror(errno)
<< endl;
exit(6);
}
cout << "close_shm success" << endl;
}
struct shmid_ds* get_imformation_of_shm()
{
static struct shmid_ds information;
int num = shmctl(_shmid,IPC_STAT,&information);
if(num == -1)
{
cout << "get_imformation_of_shm error:" << errno << " reason:" << strerror(errno) << endl;
return nullptr;
}
cout << "get_imformation_of_shm success" << endl;
return &information;
}
int get_shmid()
{
return _shmid;
}
void* get_addr()
{
return _addr;
}
private:
int _shmid;
void* _addr;
};
#define Mode 0666
#define PIPEPath "./default"
class Fifo
{
public:
Fifo(string path = PIPEPath, int mode = Mode)
:_path(path)
,_mode(mode)
{}
void CreateNamePipe()
{
int return_mkfifo_val = mkfifo(_path.c_str(),_mode);
if(return_mkfifo_val < 0)
{
cout << "mkfifo error:" << errno << " reason :" << strerror(errno)
<< endl;
exit(1);
}
cout << "mkfifo success" << endl;
}
~Fifo()
{
~Fifo()
{
int return_unlink_val = unlink(_path.c_str());
if(return_unlink_val < 0)
{
cout << "unlink error:" << errno << " reason :"
<< strerror(errno) << endl;
}
cout << "unlink namepipe success" << endl;
}
}
private:
string _path;
int _mode;
};
class SyncLast
{
public:
SyncLast()
:wfd(-1)
,rfd(-1)
{}
void CreatePipe()
{
_fifo.CreateNamePipe();
}
void WOpen()
{
wfd = open(PIPEPath, O_WRONLY);
if(wfd < 0)
{
cout << "wfd error" << endl;
exit(7);
}
}
void ROpen()
{
rfd = open(PIPEPath, O_RDONLY);
if(rfd < 0)
{
cout << "rfd error" << endl;
exit(8);
}
}
bool Wait()
{
int wait = 0;
ssize_t val = read(rfd, &wait, sizeof(wait));
if(val == sizeof(wait))
{
cout << "wait success" << endl;
}
else if(val == 0)
{
cout << "wfd close, read finish->0, exit" << endl;
return false;
}
else
{
cout << "read error" << endl;
return false;
}
return true;
}
void Wake()
{
int wake = 0;
ssize_t val = write(wfd, &wake, sizeof(wake));
if(val != sizeof(wake))
{
cout << "write error" << endl;
exit(10);
}
cout << "wake finish" << endl;
}
~SyncLast()
{
close(wfd);
close(rfd);
}
private:
Fifo _fifo;
int wfd;
int rfd;
};
最后,我们要说到的是,共享内存是最快的通信方式,那么他为什么比管道快呢?因为我们可以通过系统调用得到共享内存地址,直接进行访问,而管道就需要从用户层将数据拷贝到内核缓冲区里,再读取到用户层,这样的两次拷贝明显是不如我们直接访问共享内存的。