前言:在前面章节,我们学习了2种进程间通信方式,一个是通过继承方式的匿名管道,一个是通过让有血缘关系的进程在内存中看到同一份文件进行通信。此外,还可以在内存中开辟一块物理内存,通过页表映射到进程的进程地址空间中的共享内存段,也能进行进程间通信。
1:共享内存示意图
2:共享内存原理
原理: 在内存中开辟物理空间的共享内存,通过计算机的页表映射,映射到需要通信进程的进程地址空间中,操作系统再将该进程地址空间的虚拟地址反馈给客户端,客户端就可以拿到该共享内存的起始地址。对这块地址的操作就可以实现进程间的通信。
因此,使用共享内存通信分为三个步骤:
- 创建
- 关联进程和取消关联
- 释放共享内存
下面做出几个思考:
- 系统使用shm通信,是不是只有一对进程会使用共享内存呢? 答案:可能会有多个进程使用共享内存,因此会有多个共享内存被用来通信!
- 系统中一定会存在多个共享内存,操作系统要不要管理所有的共享内存呢? 答案:要!
- 如何管理多个共享内存呢? 答案:先描述,再组织。
- 对共享内存的管理就是对共享内存内核数据结构的管理(伪代码:struct shm),构建描述共享内存的结构体对象,和进程pcb一样!
- 进程间通信的前提是,让进程看到同一份资源!如何做到?
3:系统接口
这是创建共享内存的接口。
第一个参数我们还不知道,后面讲,
第二个参数是开辟的共享内存大小
第三个参数类似文件系统里面的O_CREAT,O_APPEND,O_EXCL,O_TRUNC,这里的 IPC_CREAT就是表示如果没有共享内存就创建,如果有就获取共享内存并且返回。
如果加上IPC_EXCL则表示如果没有共享内存就创建,如果有就报错返回。
函数的返回值是共享内存的id,为int类型。失败则返回-1。
3.1:key的含义
因为系统中可能会存在多个共享内存,如果AB进程需要通信,申请了一块共享内存,如何让两个进程都找到这块共享内存呢?这就体现了key的作用。
假设A进程是server,B进程是client,当A进程通过某个函数生成了一个key值,在申请共享内存的时候,他将key放到这个共享内存的描述结构体中,只要B进程通过某个函数,并且传参和A一样,也可以获取到一个一样的key,那么B进程拿着自己的key去与每一个共享内存结构体中的key匹配,如果匹配成功,那么B就可以和A看到同一份共享内存了,至此进程间通信的前提也就达到了。
3.2:key的获取方法
第一个参数是路径字符串,第二个参数是项目ID(可以随便设置比如0X6666);
key的本质是在内核中使用的。
因此当服务端使用shmget,是将key插入shm描述结构体中,客户端使用shmget是用key去匹配。
4:共享内存使用实例
4.1:makefile
.PHONY:all
all:server client
server:server.cc
g++ -o $@ $^ -std=c++11
client:client.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -rf server client
4.2:comm.cpp
因为客户端和服务端都需要使用shmget接口,甚至于后面还要与共享内存挂上联系,那不如直接使用一份公共文件。
4.2.1:创建共享内存与匹配共享内存接口:creatShmHelper
int createShmHelper(key_t k,int size,int flag)
{
int shmid = shmget(k,gsize,flag);
if(shmid==-1)exit(2);
return shmid;
}
int createShm(key_t k,int size)
{
//服务端
umask(0);
createShmHelper(k,size,IPC_CREAT|IPC_EXCL|0666);
}
int getShm(key_t k,int size)
{
createShmHelper(k,size,IPC_CREAT);
}
因为要保持创建的共享内存是最新的,所以对于服务端来说,第三个参数要传IPC_CREAT | IPC_EXCL,对于客户端只需要传入IPC_CREAT,因此直接设置一个creatshm函数,针对第三个参数设置为flag,可以让2个端口调用同一个函数,只是传参不同。这样代码就高效简洁了。
要注意客户端创建的时候,要给共享内存的权限0666,因为共享内存的权限为0的话,是不能获取共享内存相关结构体和删除共享内存的。
4.2.2:key获取函数
key_t getKey()
{
key_t k = ftok(PATHNAME,PROJID);
if(k == -1)
{
cerr<<errno<<":"<<strerror(errno)<<endl;
exit(1);
}
return k;
}
errno的头文件是<cstring>
4.2.3:将key转为16进制接口
string toHex(int x)
{
char buffer[64];
snprintf(buffer,sizeof(buffer),"0x%x",x);
return buffer;
}
4.3:server.cc
#include"comm.cpp"
int main()
{
key_t k = getKey();
cout<<"k :"<<toHex(k)<<endl;
int shmid = createShm(k,gsize);
cout<<"server shmid:"<<shmid<<endl;
return 0;
}
4.4:client.cc
#include"comm.hpp"
int main()
{
key_t k = getKey();
cout<<"k :"<<toHex(k)<<endl;
int shmid = getShm(k,gsize);
cout<<"client shmid:"<<shmid<<endl;
return 0;
}
4.5:展示
可以看到同一份共享内存。
前提已经保证了,如何进行通信?就是将两个进程与共享内存挂接上。
5:挂接函数shmat
第一个参数:共享内存id,也就是共享内存标识
第二个参数:指定连接的地址,可以给nullptr,让操作系统自己完成
第三个参数:取值可能为SHM_RND 或 SHM_RDONLY
返回值就是一块地址,也就是之前提到的,客户端可以拿到共享内存在进程地址空间中的起始地址。
char* attach(int shmid)
{
char* start = (char*)shmat(shmid,nullptr,0);
return start;
}
写一段这样的公共代码
int main()
{
key_t k = getKey();
cout<<"k :"<<toHex(k)<<endl;
int shmid = getShm(k,gsize);
cout<<"client shmid:"<<shmid<<endl;
char* start = attach(shmid);
char c = 'A';
while(c<'Z')
{
start[c-'A'] = c;
c++;
start[c] = '\0';
sleep(1);
}
return 0;
}
int main()
{
key_t k = getKey();
cout<<"k :"<<toHex(k)<<endl;
int shmid = createShm(k,gsize);
cout<<"server shmid:"<<shmid<<endl;
char* start = attach(shmid);
int n = 0;
while(n<=30)
{
cout<<"client -> server"<<start<<endl;
sleep(1);
n++;
}
return 0;
}
这是client和server端的代码。
运行起来后如果发现
说明是之前申请的共享内存没有释放掉,如果我们需要释放共享内存,则必须对共享内存的shmid操作而不是key,类似于打开文件是必须对文件的fd操作而不是inode。
释放命令行指令:ipcrm -m shmid
查询共享内存指::ipcs -m
可以看到成功运行起来。
6:取消挂接
使用函数
参数就是shmat返回的指针,在我们这里就是start。
7:释放共享内存
使用函数
第一个参数是shmid,共享内存标识符
第二个参数是将要采取的动作
第三个参数是指向一个保存着共享内存的模式状态和访问权限的数据结构。一般给nullptr
成功返回0,失败返回-1
关于共享内存展示的最终优化代码下一章博客放出。