【Linux】Linux进程间通讯-共享内存
参考博客:https://blog.csdn.net/sjsjnsjnn/article/details/125864580
一、system V进程间通讯
它是操作系统层面上专门为进程间通信设计的一个方案,其通信方式包括如下三种:
- system V共享内存
- system V消息队列
- system V信号量
其中共享内存和消息队列是以传输数据为目的,信号量是为了保证进程间的同步与互斥而设计的
二、system V共享内存
2.1 共享内存的基本原理
-
不同的进程想要看到同一份资源,在操作系统内部,一定是通过某种调用,在物理内存当中申请一块内存空间,然后通过某种调用,让参与通信进程“挂接”到这份新开辟的内存空间上;
-
其本质:将这块内存空间分别与各个进程各自的页表之间建立映射,再在虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应关系,至此这些参与通信进程便可以看到了同一份物理内存,这块物理内存就叫做共享内存。
2.2 共享内存的数据结构
- 我们知道在操作系统中是存在大量的进程的,如果两两进程进程进行通信,就需要多个共享内存。
- 既然共享内存在系统中存在多份,就一定要将这些不同的共享内存管理起来,即先描述,再组织;
- 为了保证两个或多个进程能够看到它们的同一份共享内存,那么共享内存一定要有能够唯一标识性的ID,方便让不同的进程识别它们的同一份共享内存;这个所谓的ID一定是在共享内存的数据结构中;
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 */
};
/*shm_perm 成员储存了共享内存对象的存取权限及其它一些信息。shm_segsz 成员定义了共享的内存大小(以字节为单位) 。shm_atime 成员保存了最近一次进程连接共享内存的时间。shm_dtime 成员保存了最近一次进程断开与共享内存的连接的时间。shm_ctime 成员保存了最近一次 shmid_ds 结构内容改变的时间。shm_cpid 成员保存了创建共享内存的进程的 pid 。shm_lpid 成员保存了最近一次连接共享内存的进程的 pid。shm_nattch 成员保存了与共享内存连接的进程数目
*/
对于每个IPC对象,系统共用一个struct ipc_perm的数据结构来存放权限信息,以确定一个ipc操作是否可以访问该IPC对象。
struct ipc_perm{__kernel_key_t key;__kernel_uid_t uid;__kernel_gid_t gid;__kernel_uid_t cuid;__kernel_gid_t cgid;__kernel_mode_t mode;unsigned short seq;
};
2.3 共享内存相关函数总览
函数原型 | 头文件 | 功能 |
---|---|---|
int shmget (key_t key, size_t size, int shmflg); | #include <sys/ipc.h> #include <sys/shm.h> | 创建共享内存 |
key_t ftok (const char *pathname, int proj_id); | #include <sys/types.h> #include <sys/ipc.h> | 获取 key |
int shmctl (int shmid, int cmd, struct shmid_ds *buf); | #include <sys/ipc.h> #include <sys/shm.h> | 控制共享内存 |
void *shmat (int shmid, const void *shmaddr, int shmflg); | #include <sys/types.h> #include <sys/shm.h> | 共享内存关联 |
int shmdt (const void *shmaddr); | #include <sys/types.h> #include <sys/shm.h> | 共享内存去关联 |
2.4 共享内存的创建
2.4.1 函数原型
创建共享内存我们需要用shmget函数,shmget函数的函数原型如下:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
函数说明:
- 得到一个共享内存标识符或创建一个共享内存对象并返回共享内存标识符
参数说明:
- 参数key:表示标识共享内存的键值
- 需要ftok函数获取
参数size:
-
表示待创建共享内存的大小
- size是要建立共享内存的长度。所有的内存分配操作都是以页为单位的。所以如果一段进程只申请一块只有一个字节的内存,内存也会分配整整一页(在32位下一页的缺省大小PACE_SIZE=4096字节);这样,新创建的共享内存的大小实际上是从size这个参数调整而来的页面大小。即如果 size为1至4096,则实际申请到的共享内存大小为4K(一页);4097到8192,则实际申请到的共享内存大小为8K(两页),依此类推。
-
参数shmflg:表示创建共享内存的方式
返回值:
- 调用成功,返回一个有效的共享内存标识符。
- 调用失败,返回-1,错误原因存于errno中。
shmflg主要和一些标志有关:
IPC_CREAT
如果共享内存不存在,则创建一个共享内存,否则直接打开IPC_EXCEL
如果共享内存不存在,则创建一个共享内存,否则报错IPC_CREATE | IPC_EXCEL
如果共享内存不存在,则创建一个更新内存,否则直接报错
我们使用IPC_CREATE | IPC_EXCEL
可以保证使用的更新内存始终是新创建的
2.4.2 代码实现
- 传入shmget函数的第一个参数key,需要我们使用ftok函数进行获取
- ftok函数的作用就是,将一个已存在的路径名pathname(此文件必须存在且可存取)和一个整数标识符proj_id转换成一个key值。
- 在使用shmget函数创建共享内存时,首先要调用ftok函数获取这个key值,这个key值会被填充进维护共享内存的数据结构当中,作为共享内存的唯一标识。
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
//把从pathname导出的信息与proj_id的低序8位组合成一个整数IPC键,传给shmget函数的key
下面的代码通过ftok
获取了一个key
,然后通过这个key
创建了一个共享内存
#include<iostream>
#include<cstring>
#include<unistd.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>
#include<cstdio>#define PATH_NAME "./"
#define PROJ_ID 0x1234
#define SIZE 4096int main(int argc,char* argv[]){key_t key = ftok(PATH_NAME,PROJ_ID);if(key < 0){perror("ftok");return 1;}int shmid = shmget(key,SIZE,IPC_CREAT | IPC_EXCL); //创建共享内存if(shmid < 0){perror("shmget");return 1;}printf("key = %u shmid = %d\n",key,shmid);return 0;
}
- 通过
shmget
函数,通过key
定位到共享内存,如果不存在则创建共享内存,大小为4096
字节 - key是共享内存的全局 “地址”,用于定位资源。
- shmid是进程内的操作 “句柄”,用于实际控制共享内存。
- 可以通过下面的
shell
指令查询到当前系统的共享内存 - -q:列出消息队列相关信息。
- -m:列出共享内存相关信息。
- -s:列出信号量相关信息
ipcs -m
ipcrm -m
用于删除共享内存,这里要指定共享内存的shmid
,比如我们刚才创建的23
- 删除后,再次查询发现已经找不到了
ipcrm -m 23
2.5 共享内存的释放
-
刚刚我们已经创建好了共享内存,当我们的进程运行完毕后,申请的共享内存依旧存在,并没有被操作系统释放。
-
实际上,管道是生命周期是随进程的,而共享内存的生命周期是随内核的,也就是说进程虽然已经退出,但是曾经创建的共享内存不会随着进程的退出而释放。
-
这说明,如果进程不主动删除创建的共享内存,那么共享内存就会一直存在,直到关机重启(system V IPC都是如此),同时也说明了IPC资源是由内核提供并维护的。
-
此时我们若是要将创建的共享内存释放,有两个方法,一就是使用命令释放共享内存,二就是在进程通信完毕后调用释放共享内存的函数进行释放。
2.5.1 使用命令释放
下面的命令可以释放对应的共享内存,加上对应的shmid
ipcrm -m 23
2.5.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
其中,第二个参数传入的常用的选项有以下三个:
宏常量 | 功能描述 |
---|---|
IPC_STAT | 将信息从与 shmid 相关联的内核数据结构复制到 buf 指向的 shmid_ds 结构中。调用者必须具有共享内存段的读权限。 |
IPC_SET | 改变共享内存的状态,把 buf 所指的 shmid_ds 结构中的 uid 、gid 、mode 复制到共享内存的 shmid_ds 结构内 |
IPC_RMID | 删除这片共享内存 |
下面的代码演示了创建一个共享内存,程序结束后释放对应的共享内存
#include<iostream>
#include<cstring>
#include<unistd.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>
#include<cstdio>#define PATH_NAME "./"
#define PROJ_ID 0x1234
#define SIZE 4096int main(int argc,char* argv[]){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); //创建共享内存if(shmid < 0){perror("shmget");return 1;}printf("key = %u shmid = %d\n",key,shmid);sleep(5);shmctl(shmid,IPC_RMID,NULL); //释放共享内存return 0;
}
- 通过一段
shell
脚本来持续监听共享内存的状态
while :; do ipcs -m;echo "##############################";sleep 1;done
2.6 共享内存的关联与去关联
2.6.1 共享内存的关联
将共享内存连接到进程地址空间需要用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
2.6.2 共享内存的去关联
取消共享内存与进程地址空间之间的关联需要用shmdt函数,shmdt函数的函数原型如下:
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
函数说明:
- 与shmat函数相反,是用来断开与共享内存附加点的地址,禁止本进程访问此片共享内存;(并不是释放共享内存)
参数说明:
- shmaddr:连接的共享内存的起始地址
返回值:
- shmdt调用成功,返回0
- shmdt调用失败,返回-1
2.6.3 代码实现
下面的代码演示了共享内存的关联与去关联,通过持续监听共享内存的方式,可以动态观察到更新内存关联数量的变化
void test2(){key_t key = ftok(PATH_NAME,PROJ_ID);if(key < 0){perror("ftok");return;}int shmid = shmget(key,SIZE,IPC_CREAT | IPC_EXCL | 0666); //创建共享内存if(shmid < 0){perror("shmget");return;}printf("key = %u shmid = %d\n",key,shmid);sleep(2);char* mem = (char*)shmat(shmid,NULL,0); //关联共享内存printf("attaches shm sucess\n");sleep(2);shmdt(mem);printf("detaches shm sucess\n"); //解除关联共享内存sleep(2);shmctl(shmid,IPC_RMID,NULL); //释放共享内存printf("release shm sucess\n");}
- 通过动态监听我们可以观察到更新内存关联数量的变化
2.7 使用共享内存通讯
- 下面的代码实现了使用共享内存的方法进行通讯
- 分为客户端和服务端,服务端负责创建并关联共享内存,客户端直接关联共享内存
- 通过不断写入和读取共享内存实现客户端和服务端之间的通讯
2.7.1 服务端实现
#include<iostream>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<cstdio>
#include<cstring>
#include<unistd.h>#define PATH_NAME "./"
#define PROJ_ID 0x6666
#define SIZE 4096int main(){key_t key = ftok(PATH_NAME, PROJ_ID);if(key < 0){perror("ftok");return 1;}int shmid = shmget(key, SIZE, IPC_CREAT | 0666); if(shmid < 0){perror("shmget");return 1;}char* mem = (char*)shmat(shmid, NULL, 0);if (mem == (char*)-1) { perror("shmat");return 1;}while(1){bool hasMem = false;for(int i=0; i < SIZE-1 && mem[i]; ++i){std::cout << mem[i];hasMem = true;}if(hasMem){std::cout << std::endl;memset(mem, 0, SIZE);}sleep(1);}shmdt(mem);shmctl(shmid, IPC_RMID, NULL);return 0;
}
2.7.2 客户端实现
#include<iostream>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<cstdio>
#include<cstring>#define PATH_NAME "./"
#define PROJ_ID 0x6666
#define SIZE 4096int main(){key_t key = ftok(PATH_NAME,PROJ_ID);if(key < 0){perror("ftok");return 1;}int shmid = shmget(key,SIZE,IPC_CREAT); //如果存在就打开,否则创建if(shmid < 0){perror("shmget");return 1;}char* mem = (char*)shmat(shmid,NULL,0);while(1){std::string msg;std::cout << "Please Enter Message : ";std::getline(std::cin,msg);const char* buffer = msg.c_str();memcpy(mem,buffer,msg.size());}shmdt(mem);return 0;
}
运行结果,左边是服务端,右边是客户端
三、总结
共享内存:
-
要使用一块共享内存,进程必须首先分配它。随后需要访问这个共享内存块的每一个进程都必须将这个共享内存绑定到自己的地址空间中。
-
在 Linux 系统中,每个进程的虚拟内存是被分为许多页面的。这些内存页面中包含了实际的数据。每个进程都会维护一个从内存地址到虚拟内存页面之间的映射关系。尽管每个进程都有自己的内存地址,不同的进程可以同时将同一个内存页面映射到自己的地址空间中,从而达到共享内存的目的。
-
分配一个新的共享内存块会创建新的内存页面。因为所有进程都希望共享对同一块内存的访问,只应由一个进程创建一块新的共享内存。再次分配一块已经存在的内存块不会创建新的页面,而只是会返回一个标识该内存块的标识符。
-
一个进程如需使用这个共享内存块,则首先需要将它绑定到自己的地址空间中。这样会创建一个从进程本身虚拟地址到共享页面的映射关系。当对共享内存的使用结束之后,这个映射关系将被删除。当再也没有进程需要使用这个共享内存块的时候,必须有一个(且只能是一个)进程负责释放这个被共享的内存页面。
-
所有共享内存块的大小都必须是系统页面大小的整数倍。系统页面大小指的是系统中单个内存页面包含的字节数。在 Linux 系统中,内存页面大小是4KB,不过您仍然应该通过调用 getpagesize 获取这个值(通过man 2 getpagesize查看 )。
-
共享内存的生命周期是随内核的,而管道是随进程的。
-
共享内存不提供任何的同步和互斥机制,需要程序员自行保证数据安全。
-
共享内存在各种进程间通信方式中具有最高的效率。访问共享内存区域和访问进程独有的内存区域一样快,并不需要通过系统调用或者其它需要切入内核的过程来完成。同时它也避免了对数据的各种不必要的复制。
更多资料:https://github.com/0voice