一、前言
共享内存是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递将不再涉及到内核,换句话说,进程将不再通过执行进入系统内核的系统调用来传递彼此的数据。
但其实比它好用的进程间通信还有很多种,但是因为有人还是会用这种方式,我们还是需要了解一下。
IPC就是进程间通信的缩写——Interprocess communication
二、共享内存的原理
这是两个进程,以及他们的虚拟地址空间,还有页表。他们都是通过页表映射打开并使用内存中的空间。
要想实现这两个进程间通信,还是需要让他们看到同一份资源,也就是我们手动在内存中开辟一段空间, 然后这两个进程能通过页表映射到同一块空间不就行了。
被开辟的这段空间位于虚拟地址空间的共享区,也叫内存映射段:
此处,内核将硬盘文件的内容直接映射到内存, 任何应用程序都可通过Linux的mmap()系统调用请求这种映射。内存映射是一种方便高效的文件I/O方式, 因而被用于装载动态共享库。如C标准库函数(fread、fwrite、fopen等)和Linux系统I/O函数,它们都是动态库函数,其中C标准库函数都被封装在了/lib/libc.so库文件中,都是二进制文件。这些动态库函数都是与位置无关的代码,即每次被加载进入内存映射区时的位置都是不一样的,因此使用的是其本身的逻辑地址,经过变换成线性地址(虚拟地址),然后再映射到内存。而静态库不一样,由于静态库被链接到可执行文件中,因此其位于代码段,每次在地址空间中的位置都是固定的。
三、需要学习的函数
shmget函数
功能:用来创建共享内存
原型:int shmget(key_t key,size_t size ,int shmflg);
我们从后往前一个个介绍它的参数。
shmflg参数:
这是调用这个函数式输入的命令,是位图结构,有九个权限标志构成。
会用到的命令:IPC_CREAT and IPC_EXCL
// 单独使用IPC_CREAT: 创建一个共享内存,如果共享内存不存在,就创建之,如果已经存在,获取已经存在的共享内存并返回
// IPC_EXCL不能单独使用,一般都要配合IPC_CREAT
// IPC_CREAT | IPC_EXCL: 创建一个共享内存,如果共享内存不存在,就创建之, 如果已经存在,则立马出错返回 -- 如果创建成功,对应的shm,一定是最新的!
应用场景:
IPC_CREAT:创建一个共享内存,如果没有的话就创建这个内存,有了的话就直接返回这个内存的shmid——用于客户端获取服务端创建的共享内存shmid。
IPC_CRAET | IPC_EXCL:一般用于服务端创建一个全新的共享内存。
size参数:
这个很简单就是设置要创建的共享内存的大小。
key参数:
在讲这个参数的时候,我们要先知道,这个参数存在的意义
这样的情况,是我们的理想情况,一个共享内存只有两个进程在使用。但是我们需要知道,一个操作系统,同时可能存在很多的进程,也可能同时存在很多被打开的共享内存。我们要如何让需要通信的两个进程准确的打开需要的共享内存呢?
就像是管理进程一样,操作系统将全部共享内存也管理起来了,也就是我们理解操作系统非常重要的原则:先描述,再组织。
先描述:
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
这就是描述一个共享内存的结构体,这里面包含了它全部的属性,比如:创建时间、谁创建的、被连接数量、size、权限、shmid、包括上面我们说的key参数也是被封装在shm_perm里面的。
再组织:
将这些共享内存通过链表结构组织起来。
回到上面的话题,我们要怎么才能让一个进程打开它需要的那个共享内存呢?
我们创建共享内存的那个进程拿一个key值去创建,在创建的时候就将key值赋值给了上面的shm_ds结构体。
就是通过key参数,getshm函数拿着这个参数一个个比对上面这个链表节点去寻找我们需要的共享内存。
当然,要实现查找,我们还是传入IPC_CREAT这个命令的。
这个key是怎么生产的呢?反正看到不是我们随便自定义的,我们需要保证它的唯一性,才不会出错————通过ftok函数
ftok函数:
功能:生成一个key值
原型:key_t ftok(const char *pathname, int proj_id);
pathname和proj_id只是用来帮助生成的key值的
需要注意的是,ftok函数生成的key_t类型的标识符并不保证唯一,因此在使用时需要结合其他方法保证标识符的唯一性,如使用进程ID作为proj_id参数,或者使用更为安全的IPC创建函数(如shmget、msgget、semget)。
shmat函数
上面的函数只是将共享内存创造出来了,还没将它映射到我们的进程虚拟地址空间,我们还使用不了。
功能:将共享内存段连接到进程地址空间
原型:void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
shmid:共享内存标识符
shmaddr:指定连接的地址,这个我们不用动,也不知道怎么用,直接传入一个nullptr进去就行了,操作系统会自动帮我们选择一个地址。
shmflg:它的两个可能取值是SHM_RND、SHM_RDONLY
返回值:成功了返回一个指针,指向共享内存开始的地址,失败返回-1.
shmdt函数
功能:将共享内存段与当前进程脱离,与shmat相对。
原型:int shmdt(const void* shmaddr);
返回值:成功返回0,失败返回-1
shmctl函数
功能:用于控制共享内存
原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
shmid shmget返回的共享内存标识码
cmd 将要采取的动作,如下
buf 指向一个保存着共享内存的模糊状态和访问权限的数据结构
返回值:成功返回0,失败-1
四、代码实例、
makefile:
.PHONY:all
all: shmclient shmserver
shmclient: shmclient.cc
g++ $@ $^ -std=c++11
shmserver: shmserver.cc
g++ $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f shmclient shmserver
comm.hpp
#ifndef __COMM_HPP__
#define __COMM_HPP__
#include<iostream>
#include<cerror>
#include<cstdio>
#include<cstring>
#include<cassert>
#include<string>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>
#include<sys/stat.h>
using namespace std;
// IPC_CREAT and IPC_EXCL
// 单独使用IPC_CREAT: 创建一个共享内存,如果共享内存不存在,就创建之,如果已经存在,获取已经存在的共享内存并返回
// IPC_EXCL不能单独使用,一般都要配合IPC_CREAT
// IPC_CREAT | IPC_EXCL: 创建一个共享内存,如果共享内存不存在,就创建之, 如果已经存在,则立马出错返回 -- 如果创建成功,对应的shm,一定是最新的!
#define PATHNAME "."
#define PROJID ox6666
const int size=4096;
//获取key
key_t getkey()
{
key_t k =ftok(PATHNAME,PROJID);
if(k==-1)
{
cout<<"error:"<<errno<<":"<<strerror(errno)<<endl;
exit(1);
}
return k;
}
string toHEX(int x)
{
char buffer[64];
snprintf(buffer,sizeof(buffer),"ox%x",x);
return buffer;
}
static int creatShmHelper(key_t key,int size,int flag)
{
int shmid=shmget(k,size,flag);
if(shmid==-1)
{
cout<<"error"<<errno<<":"<<strerror(errno)<<endl;
exit(2);
}
return shmid;
}
int createShm(key_t k,int size)
{
umask(0);
return creatShmHelper(k,size,IPC_CREAT|IPC_EXCL| 0666)
}
int getShm(key_t k,int size)
{
return creatShmHelper(k,size,IPC_CREAT);
}
char *attachShm(int chmid)
{
char *start=(char*)shmat(shmid,nullptr,0);
return start;
}
void detachShm(char *start)
{
int n=shmdt(start);
assert(n!=-1);
(void )n;
}
//用一个临时对象封装起来
#define SERVER 1
#define CLIENT 0
class Init
{
public :
init(int t):type(t)
{
key_t k=getkey();
if(type==SERVER)
{
shmid=createShm(k,size);
}
else getShm(k,size);
start=attachShm(chmid);
}
char *getStart(){ return start; }
~init()
{
detachShm(start);
if(type==SERVER)
{
delShm(shmid);
}
}
private:
char * start;
int type;//通过type辨别调用用户
int shmid;
}
shmserver.cc
#include"comm.hpp"
#include<unistd.h>
int main()
{
Init init(SERVER);
char *start=init.getStart();
int n=0;
while(m<=30)
{
cout<<"client->server#"<<start<<endl;
sleep(1);
n++;
}
return 0;
}
shmclient.cc
#include "comm.hpp"
#include <unistd.h>
int main()
{
Init init(CLIENT);
char *start = init.getStart();
char c = 'A';
while(c <= 'Z')
{
start[c - 'A'] = c;
c++;
start[c - 'A'] = '\0';
sleep(1);
}
return 0;
}