5.共享内存的释放
刚刚我们已经创建好了共享内存,当我们的进程运行完毕后,申请的共享内存依旧存在,并没有被操作系统释放。实际上,管道是生命周期是随进程的,而共享内存的生命周期是随内核的,也就是说进程虽然已经退出,但是曾经创建的共享内存不会随着进程的退出而释放。
这说明,如果进程不主动删除创建的共享内存,那么共享内存就会一直存在,直到关机重启(system V IPC都是如此),同时也说明了IPC资源是由内核提供并维护的。
此时我们若是要将创建的共享内存释放,有两个方法,一就是使用命令释放共享内存,二就是在进程通信完毕后调用释放共享内存的函数进行释放。
1.使用命令释放
[mlg@VM-20-8-centos shared_memory]$ ipcrm -m 5 //指定删除时使用的是共享内存的用户层id,即列表当中的shmid
2.使用函数释放
控制共享内存我们需要用shmctl函数,shmctl函数的函数原型如下:
#include <sys/types.h> #include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf);
函数说明:完成对共享内存的控制
参数说明:
shmctl函数的参数说明:
shmid:共享内存标识符
cmd:表示具体的控制动作
buf:共享内存管理结构体(参考上文的共享内存的数据结构)
返回值:
shmctl调用成功,返回0
shmctl调用失败,返回-1
其中,第二个参数传入的常用的选项有以下三个:
#include <stdio.h> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/types.h> #include <unistd.h> #define PATH_NAME "./" //路径名 #define PROJ_ID 0x6666 //整数标识符 #define SIZE 4096 //共享内存的大小 int main() { key_t key = ftok(PATH_NAME, PROJ_ID);//获取key值 if(key < 0){ perror("ftok"); return 1; } int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL);//创建共享内存 if(shmid < 0){ perror("shmget"); return 2; } printf("key: %u shmid: %d\n", key, shmid); sleep(10); shmctl(shmid, IPC_RMID, NULL);//释放共享内存 sleep(10); printf("key: 0x%x, shmid: %d -> shm delete success\n", key, shmid); return 0; }
通过shell脚本查看共享内存的状态:
while :; do ipcs -m;echo "##############################";sleep 1;done
通过监控脚本可以确定共享内存确实创建并且成功释放了。
上文我们提到ipcs是查看进程间通信设施的信息的,这里的perms是共享内存的权限,此时为0,表示没有任何权限,所以我们在创建共享内存的时候,想要获得权限可以如下操作:
int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); //创建权限为0666的共享内存
6.共享内存的关联(挂接)
将共享内存连接到进程地址空间需要用shmat函数,shmat函数的函数原型如下:#include <sys/types.h> #include <sys/shm.h> void *shmat(int shmid, const void *shmaddr, int shmflg);
函数说明:
连接共享内存标识符为shmid的共享内存,连接成功后把共享内存区对象映射到调用进程的地址空间,随后可像本地空间一样访问;
参数说明:
shmid |
共享内存标识符 |
shmaddr |
指定共享内存出现在进程内存地址的什么位置,一般直接指定为NULL让内核自己决定一个合适的地址位置 |
shmflg |
SHM_RDONLY:为只读模式,其他为读写模式 |
返回值:
- shmat调用成功,返回共享内存映射到进程地址空间中的起始地址
- shmat调用失败,返回(void*) -1
#include <stdio.h> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/types.h> #include <unistd.h> #define PATH_NAME "./" //路径名 #define PROJ_ID 0x6666 //整数标识符 #define SIZE 4096 //共享内存的大小 int main() { key_t key = ftok(PATH_NAME, PROJ_ID); //获取key if(key < 0){ perror("ftok"); return 1; } int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); //创建共享内存并设置权限 if(shmid < 0){ perror("shmget"); return 2; } printf("key: %u , shmid: %d\n", key, shmid); sleep(10); char* mem = (char*)shmat(shmid, NULL, 0); //休眠10s后,关联共享内存 printf("attaches shm success\n"); sleep(5); shmdt(mem); //5秒后,共享内存去关联 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; }
7.共享内存的去关联
取消共享内存与进程地址空间之间的关联需要用shmdt函数,shmdt函数的函数原型如下:
#include <sys/types.h> #include <sys/shm.h> int shmdt(const void *shmaddr);
函数说明:
与shmat函数相反,是用来断开与共享内存附加点的地址,禁止本进程访问此片共享内存;(并不是释放共享内存)
参数说明:
shmaddr:连接的共享内存的起始地址
返回值:
shmdt调用成功,返回0
shmdt调用失败,返回-1
代码同上,运行结果如下:
8.用共享内存实现serve&client通信
刚刚我们是一个进程和共享内存关联的,接下来我们让两个进程通过共享内存进行通信;在线之前我们先测试一下这两个进程能否成功挂接到同一个共享内存上;
comm.h
#pragma once #include <stdio.h> #include <sys/ipc.h> #include <sys/shm.h> #include <unistd.h> #define PATH_NAME "./" #define PROJ_ID 0x6666 #define SIZE 4097
server.c
#include "comm.h" int main() { key_t key = ftok(PATH_NAME, PROJ_ID); //获取key if(key < 0){ perror("ftok"); return 1; } int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); //创建共享内存并设置权限 if(shmid < 0){ perror("shmget"); return 2; } printf("key: %u , shmid: %d\n", key, shmid); sleep(5); char* mem = (char*)shmat(shmid, NULL, 0); //休眠10s后,关联共享内存 printf("attaches shm success\n"); sleep(5); /* 通信内容(暂时不写):先测试两个进行能不能同时挂接到同一个共享内存上 */ shmdt(mem); //5秒后,共享内存去关联 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(5); return 0; }
client.c
#include "comm.h" int main() { key_t key = ftok(PATH_NAME, PROJ_ID); if(key < 0){ perror("ftok"); return 1; } //client只需要获取即可,不需要创建 int shmid = shmget(key, SIZE, IPC_CREAT);//单独使用IPC_CREAT,共享内存存在就获取,反之创建 if(shmid < 0){ perror("shmid"); return 1; } printf("key: %u , shmid: %d\n", key, shmid); sleep(5); char* mem = (char*)shmat(shmid, NULL, 0); sleep(5); printf("client process attaches success\n"); /* 通信内容(暂时不写):先测试两个进行能不能同时挂接到同一个共享内存上 */ shmdt(mem); sleep(5); printf("client process detaches success\n"); return 0; }
从运行结果来看,两个进程确实都挂接到了共享内存;
接下来我们来实现通信内容:
//server.c while(1){ sleep(1); printf("%s\n", mem); }
服务端不断的从共享内存中读数据;
//client.c char c = 'A'; while(c < 'Z'){ mem[c - 'A'] = c; c++; mem[c - 'A'] = 0; sleep(2); }
客户端不断的向共享内存写数据;
此时先运行服务端创建共享内存,当我们运行客户端时服务端就开始不断输出数据,说明服务端和客户端是能够正常通信的。
9.共享内存的总结
共享内存:
要使用一块共享内存,进程必须首先分配它。随后需要访问这个共享内存块的每一个进程都必须将这个共享内存绑定到自己的地址空间中。
在 Linux 系统中,每个进程的虚拟内存是被分为许多页面的。这些内存页面中包含了实际的数据。每个进程都会维护一个从内存地址到虚拟内存页面之间的映射关系。尽管每个进程都有自己的内存地址,不同的进程可以同时将同一个内存页面映射到自己的地址空间中,从而达到共享内存的目的。
分配一个新的共享内存块会创建新的内存页面。因为所有进程都希望共享对同一块内存的访问,只应由一个进程创建一块新的共享内存。再次分配一块已经存在的内存块不会创建新的页面,而只是会返回一个标识该内存块的标识符。
一个进程如需使用这个共享内存块,则首先需要将它绑定到自己的地址空间中。这样会创建一个从进程本身虚拟地址到共享页面的映射关系。当对共享内存的使用结束之后,这个映射关系将被删除。当再也没有进程需要使用这个共享内存块的时候,必须有一个(且只能是一个)进程负责释放这个被共享的内存页面。
所有共享内存块的大小都必须是系统页面大小的整数倍。系统页面大小指的是系统中单个内存页面包含的字节数。在 Linux 系统中,内存页面大小是4KB,不过您仍然应该通过调用 getpagesize 获取这个值(通过man 2 getpagesize查看 )。
共享内存的生命周期是随内核的,而管道是随进程的。
共享内存不提供任何的同步和互斥机制,需要程序员自行保证数据安全。
共享内存在各种进程间通信方式中具有最高的效率。访问共享内存区域和访问进程独有的内存区域一样快,并不需要通过系统调用或者其它需要切入内核的过程来完成。同时它也避免了对数据的各种不必要的复制。
其他通信方式将会陆续补充进来