目录
一.进程间通信有哪些方式
二.什么是systemV
三.共享内存-双向通信-大致实现思路
四.4个函数about共享内存
1.shmget函数-创建
ftok函数
编辑
e.g.
ipcs/ipcrm指令(ipc资源会被回收吗)
2.shmctl函数-删除/释放
3.shmat函数-挂接
4.shmdt函数-去挂接
5.e.g.
五.总结/共享内存的特性
Q:为什么共享内存的大小建议是4KB整数倍?
六.共享内存的内核数据结构
Q:为什么我们看到的shmid是0,1,2,3,4递增的呢?
首先我们先来看一下进程间通信总共有哪些方式?
管道
System V IPC
POSIX IPC
这里我讨论的是基于systemV共享内存的进程间通信
命名管道和匿名管道都是基于文件的通信方式,还有systemV标准的通信方式:
在同一主机内的进程间通信方案——system V方案
注意进程间通信的本质:让不同进程看到同一份资源
1.通过某种调用,在内存中创建一份内存空间
2.通过某种调用,让进程(参与通信的多个进程)“挂接”到这份新开辟的内存空间上(挂接就是通过页表映射到这片物理空间)-让不同的进程看到了同一份资源
3.不用共享内存后,我们需要去关联(去掉页表中的映射)
4.释放共享内存
用于创建一个共享内存
注:
key_id实际上是一个有符号整数
key只是用来在系统层面进行标识唯一性的,不能用来管理shm
shmid是OS给用户返回的id,用来在用户层面对shm进行管理
如果硬是要类比的话,shmid就像是文件描述符,key就像是struct_file的地址,OS是使用shmid对struct_file进行管理的
这里常用两个权限标志:
pathname:路径名
proj_id:自定义项目名ID
路径名+项目名ID应该是唯一的,通过这两个参数生成的key也就是唯一的了,用于唯一标识共享内存块
1.生成同一个key
我们在server和client中使用同一个获取key值的方法ftok(并且传入相同的路径和项目ID),这样也就保证了两个不同的进程可以看到同一份资源。
int main()
{
key_t key = ftok(PATH_NAME, PROJ_ID);
if(key < 0){
perror("ftok");
return 1;
}
printf("%u\n", key);
return 0;
}
2.创建一个共享内存
int shmid = shmget(key, SIZE, IPC_CREAT|IPC_EXCL|0666); //创建全新的shm,如果和系统已经存在ID冲突,我们出错返回,这个0666表示共享内存的权限
if(shmid < 0){
perror("shmget");
return 2;
}
ipcs可以看到systemV进程通信设施状态的指令(信号量,共享内存,消息队列都属于ipc资源)
systemV的IPC资源,生命周期是随内核的,只能通过,程序员显示的释放(释放IPC资源的指令或者system call系统调用)或者是OS重启。文件,堆空间,进程退出的时候,这些资源都会被操作系统回收,但是IPC资源不会。
ipcrm指令:
用于删除ipc资源参数:
-M 以shmkey删除共享内存
-m 以shmid删除共享内存
-Q 以msgkey删除消息队列
-q 以msgid删除消息队列
-S 以semkey删除信号量
-s 以semid删除信号量
shmctl(shmid, IPC_RMID, NULL);
printf("key: 0x%x, shmid: %d -> shm delete success\n", key, shmid);
这样就能够删除shmid对应的共享内存(释放空间)
char *mem = (char*)shmat(shmid, NULL, 0);
printf("attaches shm success\n");
这样就得到了一个指向共享内存的地址mem
并不是释放共享内存,而是删除共享内存和进程的关系(删除进程页表上进程虚拟地址和共享内存物理地址映射的页表项)
shmdt(mem);
这样当前进程共享内存的挂接就解除了
这里模拟实现一个用共享队列进行通信的服务端和客户端。服务端读数据,客户端写数据
server.c
#include
#include
#include
#include
#define PATH_NAME "./" //用于ftok函数,表示当前的路径名,路径名+项目ID应该是唯一的,随之生成的key也就是唯一的
#define PROJ_ID 0x1234 //用于ftok函数,表示当前的路径名,路径名+项目ID应该是唯一的,随之生成的key也就是唯一的
#define SIZE 4097
int main()
{
key_t key = ftok(PATH_NAME, PROJ_ID);
if(key < 0){
perror("ftok");
return 1;
}
int shmid = shmget(key, SIZE, IPC_CREAT|IPC_EXCL|0666);
//创建全新的shm,加上了IPC_EXCL选项,如果和系统已经存在ID冲突,则出错返回,这个0666表示共享内存的权限
if(shmid < 0){
perror("shmget");
return 2;
}
printf("key: %u, shmid: %d\n", key, shmid);
//sleep(1);
char *mem = (char*)shmat(shmid, NULL, 0); //将共享内存挂接到当前进程
printf("attaches shm success\n");
//sleep(15);
//此处进行通信逻辑,直接像数组一样对共享内存进行操作即可,服务端进行读操作
while(1){
sleep(1);
printf("%s\n", mem); //server 认为共享内存里面放的是一个长字符串
}
shmdt(mem); //去挂接
printf("detaches shm success\n");
//sleep(5);
shmctl(shmid, IPC_RMID, NULL); //释放共享内存
printf("key: 0x%x, shmid: %d -> shm delete success\n", key, shmid);
//sleep(10);
return 0;
}
client.c
#include
#include
#include
#include
#define PATH_NAME "./"
#define PROJ_ID 0x1234
#define SIZE 4097
int main()
{
key_t key = ftok(PATH_NAME, PROJ_ID);
if(key < 0){
perror("ftok");
return 1;
}
printf("%u\n", key);
//client这里只需要进行获取即可,只需要加上IPC_CREATE选项即可
int shmid = shmget(key, SIZE, IPC_CREAT);
if(shmid < 0){
perror("shmget");
return 1;
}
char *mem = (char*)shmat(shmid, NULL, 0);
//sleep(5);
printf("client process attaches success!\n");
//此处进行通信逻辑,直接像数组一样对共享内存进行操作即可,客户端进行写操作,每隔两秒,多写入一个字母
char c = 'A';
while(c <= 'Z'){
mem[c-'A'] = c;
c++;
mem[c-'A'] = 0;
sleep(2);
}
shmdt(mem);
//sleep(5);
printf("client process detaches success\n");
//client不需要释放共享内存的空间,由服务端释放
return 0;
}
运行结果:我们2s写一次,1s读一次,我们读的时候并没有把数据拿走,所以会读两次
这里server会一直读取共享内存中的内容,即使共享内存中没有内容可以读,所以不会退出while循环,也不会执行去挂接和释放共享内存的操作。(需要后期自己用ipcrm指令删除,也可以考虑在循环中定义一个计数器,几s后自动退出循环,进行去挂接,释放共享内存的操作)
这里我们读的时候直接读的是mem这个连续的地址空间,和前面管道中的不同,管道是基于文件的,在一次打开中,读过的内容不会再读了。管道中的内容都不会像普通文件一样保存在磁盘中,读取过的数据就会失效,不能重复读取(包括匿名管道和命名管道)
1.我们在使用共享内存的时候,没有调用类似pipe or fifo中的read过样的接口,因为匿名管道和命名管道都是基于文件的,read和write也是基于文件的,本质是将数据从内核拷贝到用户,或者从用户拷贝到内核。共享内存并不是基于文件的。
2.共享内存一旦建立好并映射进自己进程的地址空间,进行通信的进程就可以直接看到该共享内存,就如同自己malloc的空间一般,不需要任何系统调用接口,直接当数组用都行。所以共享内存是所有的进程间通信中速度最快的!(共享内存最多拷贝一次,管道可能要4次或者更多)
3.当client没有写入,甚至没有启动的时候,server端也会直接读取shm,不会像管道那样等待client写入。共享内存不提供任何司步或者互斥儿制,需要程序员自行保证数据的安全!
共享内存在内核中申请的基本单位是页,内存页,大小为4KB,内核给你的大小是4KB的整数倍。比如我申请4097个字节->内核会给你4096byte*2的空间
用户层:是操作系统层的子集
ipc_perm中存放的uid是用户id,gid是组id,key就是我们传入的用于唯一标识共享内存块的id。
除此之外还有当前共享内存的一些基本信息
在内核中,所有的ipc资源都是通过数组组织起来的,这三种资源结构体都不一样,怎么放在同一个数组中呢?原因就是下面的ipc_perm
SystemV三种通信方式的ipc资源,描述ipc资源的数据结构很相似,而且第一个成员都是ipc_perm,这个ipc_perm是完全相同的。
这个数组实际上是一个ipc_perm*类型的数组,也就是一个指针数组,然后根据对应的类型强制类型转换。(用C语言实现的切片技术)
shmid为什么是0,1,2,3,4递增的呢?因为说白了这个shmid就是ipc_perm*数组的下标,共享内存是ipc资源,所有ipc资源都是用一个ipc_perm*数组管理的,shmid就是直接取了这个数组的下标。