1.system V进程间通信
管道通信本质是基于文件的,也就是说操作系统并没有为此做过多的设计工作,而system V IPC是操作系统特地设计的一种通信方式。但是不管怎么样,它们的本质都是一样的,都是在想尽办法让不同的进程看到同一份由操作系统提供的资源。
system V IPC提供的通信方式有以下三种:
- system V共享内存(重点)
- system V消息队列
- system V信号量
其中,system V共享内存和system V消息队列是以传送数据为目的的,而system V信号量是为了保证进程间的同步与互斥而设计的,虽然system V信号量和通信好像没有直接关系,但属于通信范畴。
- system V共享内存和system V消息队列就类似于手机,用于沟通信息;
- system V信号量就类似于下棋比赛时用的棋钟,用于保证两个棋手之间的同步与互斥。
2.system V共享内存
共享内存,是一种进程间通信解决方案,并且是所有解决方案中最快的一个,在通信速度上可以做到一骑绝尘
这是 System V
标准中一个比较成功的通信方式,特点就是非常快
2.1.共享内存的基本原理
首先我们要明白,共享内存是为了让进程之间进行通信,所以共享内存一定也遵守着让不同进程看到同一份资源 的原则,而共享内存可以让毫不相干的进程之间进行通信。
共享内存让不同进程看到同一份资源的方式就是:在物理内存中开辟一块公共区域,让两个不同的进程的虚拟地址同时对此空间建立映射关系,此时两个独立的进程能看到同一块空间,可以直接对此空间进行【写入或读取】,这块公共区域就是 共享内存
接下来来逐一讲解这个过程
注意:
这里所说的开辟物理空间、建立映射等操作都是调用系统接口完成的,也就是说这些动作都由操作系统来完成。
如上图,这是两个普通进程A和B在系统上工作的原理图,现在想让进程A和B之间进行通信,共享内存的方法是如何做的呢?
首先由通信的其中一方负责向系统申请共享内存,这里就让进程A负责好了,OS收到请求后,在物理内存划出一块内存区域,用来这保证了进程A和进程B能够看到并使用同一块内存空间,如下图的红色操作
共享内存申请好了,但是并不意味着就可以直接用了,因为进程A,B的页表里并没有关于共享内存区域的映射,因此,进程A和B要分别与共享内存区域进行挂接,挂接的过程就是将共享内存区域的物理地址添加到进程的页表映射中,这样进程就能通过页表映射到共享内存区域了,如下图的蓝色操作
不过这个挂接可是没那么简单,操作系统首先将物理内存的共享内存区域映射到虚拟地址空间的共享区内,获得一些虚拟地址空间,然后再通过页表将虚拟地址映射到对应的物理内存,这个和动态库的加载是一个原理
这样子共享内存就存在与各自的共享区内了
关于共享区:共享区作为虚拟地址空间中一块缓冲区域,既可作为堆栈生长扩展的区域,也可用来存储各种进程间的公共资源,比如这里的共享内存,以及之前学习的动态库,相关信息都是存储在共享区中
等到挂接完成后,进程A和B就能看到并使用同一块内存空间了,至此就可以开始通信,等到通信结束之后,通信双方要分别取消掉对共享内存区域的挂接操作,如下图绿色操作
取消挂接了并不算彻底结束了,因为共享内存的申请是直接在物理内存上进行的,不会随着进程的退出而释放,只有手动释放,或者系统重启的时候才会释放,因此,进程不再通信后,应当由共享内存申请方在进程退出前释放共享内存,如下图黄色操作
至此,共享内存的原理已经完成,总共分成了4个步骤实现共享内存通信
总结下来就说下面这些步骤
共享内存的建立大致包括以下两个过程:
- 在物理内存当中申请共享内存空间。
- 将申请到的共享内存挂接到地址空间,即建立映射关系。
共享内存的释放大致包括以下两个过程:
- 将共享内存与地址空间去关联,即取消映射关系。
- 释放共享内存空间,即将物理内存归还给系统。
注意:
这里所说的开辟物理空间、建立映射等操作都是调用系统接口完成的,也就是说这些动作都由操作系统来完成。
2.2. 描述共享内存的结构体
在正式使用共享内存通信之前,需要先学习一下 共享内存的相关知识,因为这里的共享内存出自System V标准,所以System V中的消息队列、信号量绝大部分接口的风格也与之差不多
共享内存不止用于两个进程间通信,所以共享内存必须确保能持续存在,这也就意味着共享内存的生命周期不随进程,而是随操作系统,一旦共享内存被创建,除非被删除,否则将会一直存在,因此 操作系统需要对共享内存的状态加以描述
共享内存也不止存在一份,当出现多块共享内存时,操作系统不可能一一比对进行使用,秉持着高效的原则,操作系统会把已经创建的共享内存组织起来,更好的进行管理
所以共享内存需要有自己的数据结构,经过操作系统 先描述,再组织 后,构成了下面这个数据结构
注:shm 表示共享内存
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 */ };
当我们申请了一块共享内存后,为了让要实现通信的进程能够看到同一个共享内存,因此每一个共享内存被申请时都有一个key值,这个key值用于标识系统中共享内存的唯一性。
这里需要解释一下key,共享内存是用来进程间通信的,那么系统中那么多进程,肯定会存在很多的共享内存,那么系统要管理这些共享内存就要给这些共享内存标号,标明它的唯一性,这个key值就是这段共享内存在系统中的唯一性编号,通过这个唯一性编号,以及你要申请的共享内存的大小,系统就可以帮你申请一块共享内存了。
key_t 实际就是对 int 进行了封装,表示一个数字,用来标识不同的共享内存块,可以理解为共享内存的 inode。
你怎么保证让不同的进程看到同一个共享内存呢?你怎么知道这个共享内存存在还是不存在呢?都要借助key。只要第一个进程通过key创建共享内存,第二个之后的进程只要拿着同一个key就可以和第一个进程看到同一块共享内存了
可以看到上面共享内存数据结构的第一个成员是shm_perm,shm_perm是一个ipc_perm类型的结构体变量,每个共享内存的key值存储在shm_perm这个结构体变量当中,其中ipc_perm结构体的定义如下:
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; };
共享内存虽然属于文件系统,但它的结构是经过特殊设计的,与文件系统中的 inode
那一套结构逻辑不一样
共享内存的数据结构
shmid_ds
和ipc_perm
结构体分别在/usr/include/linux/shm.h和/usr/include/linux/ipc.h中定义。
再者,操作系统怎么知道有多少个进程在使用这个共享内存呢?那么很简单,肯定有一个类似引用计数的东西
3.使用共享内存
3.1.共享内存的创建
创建共享内存我们需要用shmget函数,shmget函数的函数原型如下:
shmget函数的参数说明:
- 第一个参数key,表示待创建共享内存在系统当中的唯一标识。
- 第二个参数size,表示待创建共享内存的大小。
- 第三个参数shmflg,表示创建共享内存的方式。
shmget函数的返回值说明:
- shmget调用成功,返回一个有效的共享内存标识符(用户层标识符)。
- shmget调用失败,返回-1。
注意: 我们把具有标定某种资源能力的东西叫做句柄,而这里shmget函数的返回值实际上就是共享内存的句柄,这个句柄可以在用户层标识共享内存,当共享内存被创建后,我们在后续使用共享内存的相关接口时,都是需要通过这个句柄对指定共享内存进行各种操作。
返回值
因为共享内存拥有自己的数据结构,所以 返回值 int 实际就是 shmid,类似于文件系统中的 fd,用来对不同的共享内存块进行操作,
传入shmget函数的第一个参数key,需要我们使用ftok函数进行获取
这里需要解释一下key,共享内存是用来进程间通信的,那么系统中那么多进程,肯定会存在很多的共享内存,那么系统要管理这些共享内存就要给这些共享内存标号,标明它的唯一性,这个key值就是这段共享内存在系统中的唯一性编号,通过这个唯一性编号,以及你要申请的共享内存的大小,系统就可以帮你申请一块共享内存了。
key_t 实际就是对 int 进行了封装,表示一个数字,用来标识不同的共享内存块,可以理解为 inode
你怎么保证让不同的进程看到同一个共享内存呢?你怎么知道这个共享内存存在还是不存在呢?都要借助key。只要第一个进程通过key创建共享内存,第二个之后的进程只要拿着同一个key就可以和第一个进程看到同一块共享内存了。
所以第一次创建的时候,必须有一个key。
- 我们在命名管道怎么确定是同一个命名管道的?
是通过同一路径下面+同一文件名
传入shmget函数的第一个参数key,需要我们使用ftok函数进行获取
ftok函数的作用就是,将一个已存在的路径名pathname和一个项目标识符转换成一个key值,称为IPC键值,在使用shmget函数获取共享内存时,这个key值会被填充进维护共享内存的数据结构当中。
ftok函数可以理解为一套算法,像加法一样,传数据进去,它只是搞计算的,除了生成一个结果,别的东西它不干, 将pathname 和 proj_id当成数据通过算法生成了一个序号id
简单来说就是给一个文件路径名和一个int值,那么ftok函数就会生成一个能唯一标识共享内存的key值,也就是shmget()函数中第一个参数的key值
需要注意的是,pathname所指定的文件必须存在且可存取。
注意:
- 我们说ftok只是一套算法,使用ftok函数生成key值可能是一样的,就像1+8,2+7和3+6的结果都是9,同样,我们传入不同的pathname和proj_id也可能会这样子,算出的key值相同。
- 我们上面说只要第一个进程通过key创建共享内存,第二个之后的进程只要拿着同一个key就可以和第一个进程看到同一块共享内存了。
- 所以需要进行通信的各个进程,在使用ftok函数获取key值时,只要我们传入相同的pathname和proj_id就能得到相同的key值,然后才能找到同一个共享资源。
- 1.为什么要让我们来设置key值?明明让用户干会有冲突啊!
重要的是,如果操作系统为你这个进程生成了一个key,那么你怎么把这个key传递给另一个进程呢?操作系统怎么知道你要和谁通信?
与其说是用户来设定的,不如说是用户来约定的,这样子用户可以自由指定哪些用户来通信!
- 2.当shmget()三个参数齐全,创建共享内存成功的时候,就会返回一个共享内存的标识码shmid,可能你会惊讶,刚才用ftok已经生成了标识共享内存的码,怎么这里又返回了一个?key和shmid是一样的吗?
其实这两个码都可以用来标识共享内存,shmid与key的关系就类似于文件系统中的fd和inode,
- key就像inode一样,是给系统看的,操作系统通过key来标识这个共享内存
- shmid和fd类似,是给应用层的进程使用的,只在进程里存在,是进程用来控制这个共享内存的途径
我们刚开始得通过key获得shmid,之后进程就一直使用shmid来管理共享内存了
- 3.为何要多此一举呢?
这是为了系统层和应用层之间的解耦,避免因应用层的shmid出现错误而影响了系统层的正常工作
参数2为创建共享内存的大小,单位是字节,一般设为 4096 字节(4kb),与一个 PAGE 页大小相同,有利于提高 IO 效率
如果size设置成4097 ,在OS底层给你分配了2页(按页对齐),但是你要4097字节那么我就只让你看到4097个字节的空间,绝对不少给你但也不多给你,少给了可能会出问题,多给了也可能出问题,用户要我怎么办我就怎么办,严格按照用户来 ; 所以最好设置4096的整数倍。
参数3是位图结构,类似于 open 函数中的参数3(文件打开方式),常用的选项有以下几个:
- IPC_CREAT 创建共享内存,如果存在,则使用已经存在的
- IPC_EXCL 避免使用已存在的共享内存,不能单独使用,需要配合 IPC_CREAT 使用,作用是当创建共享内存时,如果共享内存已经存在,则创建失败
- 权限 因为共享内存也是文件,所以权限可设为文件的起始权限 0666
参数3常用的组合方式:
- IPC_CREAT 如果不存在键值与key相等的共享内存,则新建一个共享内存并返回该共享内存的句柄;如果存在这样的共享内存,则直接返回该共享内存的句柄
- IPC_CREAT | IPC_EXCL 如果不存在键值与key相等的共享内存,则新建一个共享内存并返回该共享内存的句柄;如果存在这样的共享内存,则出错返回
换句话说:
- 使用组合IPC_CREAT,一定会获得一个共享内存的句柄,但无法确认该共享内存是否是新建的共享内存。
- 使用组合IPC_CREAT | IPC_EXCL,只有shmget函数调用成功时才会获得共享内存的句柄,并且该共享内存一定是新建的共享内存。
我们就可以写出下面这些接口了
#pragma once #include<iostream> #include <unistd.h> #include <sys/ipc.h> #include <sys/shm.h> #include <string> #include <fcntl.h> #include <cerrno> #include<cstdlib> using namespace std; const string pathname="/home/zs_108/A";//我的processa所在地 const int proj_id=0x8088;//随便取的 const int SIZE=4096;//4kb key_t Getkey()//获取key值 { key_t key=ftok(pathname.c_str(),proj_id); if(key<0) { perror("Getkey failed"); exit(1);//没key玩个屁啊 } printf("key: %x\n", key); //打印key值 return key; } int GetShareMem()//获取共享内存 { int shmid=shmget(Getkey(),SIZE, IPC_CREAT | IPC_EXCL); //创建新的共享内存 if(shmid<0) { perror("Creat failed"); exit(2);//共享内存都创建不出来,玩什么呢? } printf("shm: %d\n", shmid); //打印句柄 return shmid; }
我们补充一个小知识
Linux当中,我们可以使用
ipcs
命令查看有关进程间通信设施的信息。单独使用
ipcs
命令时,会默认列出消息队列、共享内存以及信号量相关的信息,若只想查看它们之间某一个的相关信息,可以选择携带以下选项:
- -q:列出消息队列相关信息。
- -m:列出共享内存相关信息。
- -s:列出信号量相关信息。
例如,携带-m选项查看共享内存相关信息:
此时,根据
ipcs
命令的查看结果和我们的输出结果可以确认,共享内存已经创建成功了。
ipcs
命令输出的每列信息的含义如下:注意: key是在内核层面上保证共享内存唯一性的方式,而shmid是在用户层面上保证共享内存的唯一性,key和shmid之间的关系类似于inode和fd之间的的关系。
我们先拿个东西测试一下
processa.cpp
#include"common.h" int main() { int shmid=GetShareMem();//创建共享内存 sleep(20); }
我们再运行一下
报错了,这是共享内存在进程退出后还存在!!!!
3.2.共享内存的释放
通过上面创建共享内存的实验可以发现,当我们的进程运行完毕后,申请的共享内存依旧存在,并没有被操作系统释放。
实际上,管道是生命周期是随进程的,而共享内存的生命周期是随内核的,也就是说进程虽然已经退出,但是曾经创建的共享内存不会随着进程的退出而释放。
这说明,进程虽然已经退出,但是曾经创建的共享内存不会随着进程的退出而释放,如果进程不主动删除创建的共享内存,那么共享内存就会一直存在,直到关机重启(system V IPC都是如此),同时也说明了IPC资源是由内核提供并维护的。
此时我们若是要将创建的共享内存释放,有两个方法,
- 一就是使用命令释放共享内存,
- 二就是在进程通信完毕后调用释放共享内存的函数进行释放。
- 使用命令释放共享内存资源
我们可以使用ipcrm -m shmid命令释放指定id的共享内存资源。
注意: 指定删除时使用的是共享内存的用户层id,即列表当中的shmid。
- 为什么不是key呢?
你是用户,用户层统一使用shmid,只有操作系统能用key啊!!!!
我们可以使用这个来解决上面那个问题
使用程序释放共享内存资源
还有一个系统调用接口可以用来释放共享内存资源
参数:
- 第一个参数shmid,表示所控制共享内存的用户级标识符(shmid——shmget的返回值)。
- 第二个参数cmd,表示具体的控制动作。
- 第三个参数buf,用于获取或设置所控制共享内存的数据结构。
返回值:
- shmctl调用成功,返回0。
- shmctl调用失败,返回-1。
shmctl函数的第二个参数传入的常用的选项:
例如,在以下代码当中,共享内存被创建,两秒后程序自动移除共享内存,再过两秒程序就会自动退出。
processa.cpp
#include"common.h" int main() { int shmid=GetShareMem();//创建共享内存 sleep(2); shmctl(shmid, IPC_RMID, NULL); //释放共享内存 sleep(2); }
我们可以在程序运行时,使用以下监控脚本时刻关注共享内存的资源分配情况:
while :; do ipcs -m;echo "###################################";sleep 1;done
通过监控脚本可以确定共享内存确实创建两秒后成功释放了。
3.2.共享内存的关联
共享内存在被成功创建后,进程还不 “认识” 它,只有让待通信进程都 “认识” 同一个共享内存后,才能进行正常通信,让进程 “认识” 共享内存这一操作称为 关联
当进程与共享内存关联后,共享内存才会 通过页表映射至进程的虚拟地址空间中的共享区中
将共享内存连接到进程地址空间我们需要用shmat函数,shmat函数的函数原型如下:
shmat函数的参数说明:
- 第一个参数shmid,表示待关联共享内存的用户级标识符(shmget的返回值)。
- 第二个参数shmaddr,指定共享内存映射到进程地址空间的某一地址,通常设置为NULL,表示让内核自己决定一个合适的地址位置。
- 第三个参数shmflg,表示关联共享内存时设置的某些属性。
共享内存映射至共享区时,我们可以指定映射位置(即传递参数2),但我们一般不知道具体地址,所以 可以传递
NULL
,让编译器自动选择位置进行映射
第三个参数shmflg传入的常用的选项:
选项 | 作用 |
SHM_RDONLY | 关联共享内存后只进行读取操作 |
SHM_RND | 若shmaddr不为NULL,则关联地址自动向下调整为SHMLBA的整数倍。公式:shmaddr-(shmaddr%SHMLBA) |
0 | 默认为读写权限 |
shmat函数的返回值说明:
- shmat调用成功,返回共享内存映射到进程地址空间中的起始地址。
- shmat调用失败,返回(void*)-1。
这时我们可以尝试使用shmat函数对共享内存进行关联。
#include"common.h" int main() { int shmid=GetShareMem();//创建共享内存 printf("attach begin!\n"); sleep(2); char* mem = (char*)shmat(shmid, NULL, 0); //关联共享内存 if (mem == (void*)-1){ perror("shmat"); return 1; } printf("attach end!\n"); sleep(2); }
代码运行后发现关联失败
主要原因是我们使用shmget函数创建共享内存时,并没有对创建的共享内存设置权限,所以创建出来的共享内存的默认权限为0,即什么权限都没有,因此server进程没有权限关联该共享内存。
- 那么怎么设置权限呢?
shmget函数创建共享内存时,在其第三个参数处设置共享内存创建后的权限,权限的设置规则与设置文件权限的规则相同。
int shmid = shmget(Getkey, SIZE, IPC_CREAT | IPC_EXCL | 0666); //创建权限为0666的共享内存
而共享内存的权限(perms)显示也不再是0,而是我们设置的666权限。
我们看例子
processa.cpp
#include"common.h" int main() { int shmid=GetShareMem();//创建共享内存 sleep(2); }
这样子我们的共享内存就有权限了!!!!!就能被别人访问了!!!!
为了优化我们的使用,我们对GetShareMemHelper函数进行优化
#pragma once #include<iostream> #include <unistd.h> #include <sys/ipc.h> #include <sys/shm.h> #include <string> #include <fcntl.h> #include <cerrno> #include<cstdlib> using namespace std; const string pathname="/home/zs_108/A";//我的processa所在地 const int proj_id=0x7777;//随便取的 const int SIZE=4096;//4kb key_t Getkey()//获取key值 { key_t key=ftok(pathname.c_str(),proj_id); if(key<0) { perror("Getkey failed"); exit(1);//没key玩个屁啊 } printf("key: %x\n", key); //打印key值 return key; } int GetShareMemHelper(int flag)//获取共享内存 { int shmid=shmget(Getkey(),SIZE, flag); //创建新的共享内存 if(shmid<0) { perror("Creat failed"); exit(2);//共享内存都创建不出来,玩什么呢? } printf("shm: %d\n", shmid); //打印句柄 return shmid; } int CreatShm()//负责创建共享内存 { return GetShareMemHelper(IPC_CREAT | IPC_EXCL|0666); } int GetShm()//单纯的使用已经存在的共享内存 { return GetShareMemHelper(IPC_CREAT); }
我们将这个创建和使用分开来
我们现在来看看
processa.cpp
#include"common.h" int main() { int shmid=CreatShm();//创建共享内存 printf("attach begin!\n"); sleep(2); char* mem = (char*)shmat(shmid, NULL, 0); //关联共享内存 if (mem == (void*)-1){ perror("shmat"); return 1; } printf("attach end!\n"); sleep(2); }
运行看看
很好
注意: 程序运行结束后,会自动取消关联状态
3.4.共享内存的去关联
如同关闭FILE*、fd、free等一些列操作一样,当我们关联共享内存,使用结束后,需要进行去关联,否则会造成内存泄漏(指针指向共享内存,访问数据)
取消共享内存与进程地址空间之间的关联我们需要用shmdt函数,shmdt函数的函数原型如下:
这个函数使用非常简单,将已关联的共享内存地址传递进行去关联即可
shmdt函数的参数说明:
- 待去关联共享内存的起始地址,即调用shmat函数时得到的起始地址。也就是直接传shmat的返回值进去即可
shmdt函数的返回值说明:
- shmdt调用成功,返回0。
- shmdt调用失败,返回-1。
注意:
共享内存在被删除后,已成功挂接的进程仍然可以进行正常通信,不过此时无法再挂接其他进程
共享内存被提前删除后,状态
status
变为 销毁dest
现在我们就能够取消共享内存与进程之间的关联了。
#include"common.h" int main() { int shmid=CreatShm();//创建共享内存 printf("attach begin!\n"); sleep(2); char* mem = (char*)shmat(shmid, NULL, 0); //关联共享内存 if (mem == (void*)-1){ perror("shmat"); return 1; } printf("attach end!\n"); sleep(2); printf("detach begin!\n"); shmdt(mem); //共享内存去关联 printf("detach end!\n"); sleep(2); }
运行程序,通过监控即可发现该共享内存的关联数由1变为0的过程,即取消了共享内存与该进程之间的关联。
注意: 将共享内存段与当前进程脱离不等于删除共享内存,只是取消了当前进程与该共享内存之间的联系。
4.用共享内存实现通信
先来复习一下makefile
为了让procesa和processb在使用ftok函数获取key值时,能够得到同一种key值,那么procesa和processb传入ftok函数的路径名和和整数标识符必须相同,这样才能生成同一种key值,进而找到同一个共享资源进行挂接。这里我们可以将这些需要共用的信息放入一个头文件当中,procesa和processb共用这个头文件即可。
common.h
#pragma once #include<iostream> #include <unistd.h> #include <sys/ipc.h> #include <sys/shm.h> #include <string> #include <fcntl.h> #include <cerrno> #include<cstdlib> #include<cstring> using namespace std; const string pathname="/home/zs_108/A";//我的processa所在地 const int proj_id=0x7777;//随便取的 const int SIZE=4096;//4kb key_t Getkey()//获取key值 { key_t key=ftok(pathname.c_str(),proj_id); if(key<0) { perror("Getkey failed"); exit(1);//没key玩个屁啊 } printf("key: %x\n", key); //打印key值 return key; } int GetShareMemHelper(int flag)//获取共享内存 { int shmid=shmget(Getkey(),SIZE, flag); //创建新的共享内存 if(shmid<0) { perror("Creat failed"); exit(2);//共享内存都创建不出来,玩什么呢? } printf("shm: %d\n", shmid); //打印句柄 return shmid; } int CreatShm()//负责创建共享内存 { return GetShareMemHelper(IPC_CREAT | IPC_EXCL|0666); } int GetShm()//单纯的使用已经存在的共享内存 { return GetShareMemHelper(IPC_CREAT); }
在知道了共享内存的创建、关联、去关联以及释放后,现在可以尝试让两个进程通过共享内存进行通信了。在让两个进程进行通信之前,我们可以先测试一下这两个进程能否成功挂接到同一个共享内存上。
processa负责创建共享内存,创建好后将共享内存和processa进行关联,之后进入死循环,便于观察processa是否挂接成功。
processa代码如下:
#include"common.h" int main() { int shmid=CreatShm();//创建共享内存 char* shmaddr = (char*)shmat(shmid, NULL, 0); //关联共享内存 if (shmaddr == (void*)-1){ perror("shmat failed"); return 1; } while(true)//通信部分 { ; } shmdt(shmaddr); //共享内存去关联 shmctl(shmid,IPC_RMID,nullptr);//删除共享内存 }
我们可以运行一下看看有没有连接成功
while :; do ipcs -m;echo "###################################";sleep 1;done
挂接成功了啊
processb只需要直接和processa创建的共享内存进行关联即可,之后也进入死循环,便于观察processb是否挂接成功。
processb.cpp代码如下:
#include"common.h" int main() { int shmid=GetShm();//获取共享内存 char* shmaddr = (char*)shmat(shmid, NULL, 0); //关联共享内存 if (shmaddr == (void*)-1){ perror("shmat failed"); return 1; } while(true)//通信部分 { ; } shmdt(shmaddr); //共享内存去关联 }
先后运行processa和processb后,通过监控脚本可以看到processa和processb所关联的是同一个共享内存,共享内存关联的进程数也是2,表示processa和processb挂接共享内存成功。
此时我们就可以让processa和processb进行通信了,这里以简单的发送字符串为例。
进程要怎么读取/写入?调用系统调用接口read/write?共享内存已经在我们的进程的进程地址空间里了,就是让我们使用虚拟地址来访问即可!!!这个和使用堆的内存是一样的,我们可以把这个shmat和malloc进行类比,shmat获取共享内存的首地址,malloc获得分配的堆内存的首地址
processb.cpp
include"common.h" int main() { int shmid=GetShm();//获取共享内存 char* shmaddr = (char*)shmat(shmid, NULL, 0); //关联共享内存 if (shmaddr == (void*)-1){ perror("shmat failed"); return 1; } while(true)//通信部分 { cout<<"Please Enter@"; fgets(shmaddr,4096,stdin); } shmdt(shmaddr); //共享内存去关联 }
processa.cpp
#include"common.h" int main() { int shmid=CreatShm();//创建共享内存 char* shmaddr = (char*)shmat(shmid, NULL, 0); //关联共享内存 if (shmaddr == (void*)-1){ perror("shmat failed"); return 1; } while(true)//通信部分 { cout<<"processb say@"<<shmaddr<<endl;//读取共享内存的内容 sleep(1); } shmdt(shmaddr); //共享内存去关联 shmctl(shmid,IPC_RMID,nullptr);//删除共享内存 }
我们先运行一下啊
我们processb还没有启动嘞!!!processa就一直运行!!!!
接下来我们运行一下processb,来运行一下
我们发现确实能通信,但是我们也发现一些端倪,我输入的慢一点,左边的怎么还会重复打印我上一次输入的内容?,这个是共享内存的特点——内存!!!可以存东西的,可不是管道那样不能存数据的
5.共享内存的特点
- (1)、共享内存无同步,无互斥之类的保护机制
- (2)、共享内存是所有进程间通信速度最快的。
- (3)、共享内存的生命周期随内核。
6.共享内存与管道进行对比
6.1.通信速度比较
当共享内存创建好后就不再需要调用系统接口进行通信了(直接对地址空间进行操作),而管道创建好后仍需要read、write等系统接口进行通信。实际上,共享内存是所有进程间通信方式中最快的一种通信方式
6.2.数据拷贝过程
read是把数据从内核缓冲区复制到进程缓冲区 , write是把进程缓冲区复制到内核缓冲区
当共享内存创建好后就不再需要调用系统接口进行通信了,而管道创建好后仍需要read、write等系统接口进行通信。
我们先来看看管道通信:
从这张图可以看出,使用管道通信的方式,将一个文件从一个进程传输到另一个进程需要进行四次拷贝操作:
- 服务端将信息从输入文件复制到服务端的临时缓冲区中。
- 将服务端临时缓冲区的信息复制到管道中。
- 客户端将信息从管道复制到客户端的缓冲区中。
- 将客户端临时缓冲区的信息复制到输出文件中。
我们再来看看共享内存通信:
从这张图可以看出,使用共享内存进行通信,将一个文件从一个进程传输到另一个进程只需要进行两次拷贝操作:
- 从输入文件到共享内存。
- 从共享内存到输出文件。
7.共享内存的补充知识
7.1、共享内存的大小
在上面的代码中,我们将共享内存的大小设为 4096 字节,即一个 PAGE 页的大小(4kb);如果申请 4097 字节大小的共享内存,操作系统实际上会分配 8192 字节(8kb 的空间),但供共享内存使用的只有 4097 字节
为什么会出现这种现象?
- 因为操作系统为了避免因非法操作导致出现越界访问问题,所以会开辟 PAGE 页的整数倍大小空间,多开辟的空间不会给共享内存时,主要是用来检测是否出现了越界访问
7.2.为什么共享内存是速度最快的IPC方法?
- ① 共享内存的拷贝次数少
- ② 在使用共享内存时不涉及系统调用接口(也就是不会有内核态到用户态之间的转化,因为都是在用户层进行操作的)
- ③ 不提供任何保护机制(没有同步与互斥)
7.3、共享内存的缺点
共享内存这么快,为什么不直接只使用共享内存呢?
因为快是要付出代价的,因为 “快” 导致共享内存有以下缺点:
多个进程无限制地访问同一块内存区域,导致共享内存中的数据无法确保安全
即 共享内存 没有同步和互斥机制,某个进程可能数据还没写完,就被别人读走了,或者被别人覆盖了
总的来说,不加规则限制的共享内存是不推荐使用的
7.5.、获取共享内存的数据结构
System V标准中还为共享内存提供了一个控制函数shmctl,其原型如下图所示:
我们可以看看第3个参数
要控制共享内存,就得知道它的所有属性
之前在释放共享内存时,我们就已经使用过了 shmctl,给参数2传入的是 IPC_RMID,表示删除共享内存,除此之外,还可以给参数2传递以下动作:
- IPC_STAT 用于获取或设置所控制共享内存的数据结构
- IPC_SET 在进程有足够权限的前提下,将共享内存的当前关联值设置为 buf 数据结构中的值
buf 就是共享内存的数据结构,可以使用 IPC_STAT 获取,也可以使用 IPC_SET 设置
当参数2为 IPC_RMID 时,参数3可以不用传递;其他两种情况都需传递 struct shmid_ds *buf
演示代码:通过 shmctl 获取共享内存的数据结构,并从中获取 pid、key
common.h
#include <iostream> #include <cerrno> #include <cstring> #include <unistd.h> #include <sys/ipc.h> #include <sys/shm.h> using namespace std; #define PATHNAME "." // 项目名 #define PROJID 0x29C // 项目编号 const int gsize = 4096; const mode_t mode = 0666; //将十进制数转为十六进制数 string toHEX(int x) { char buffer[64]; snprintf(buffer, sizeof buffer, "0x%x", x); return buffer; } // 获取key key_t getKey() { key_t key = ftok(PATHNAME, PROJID); if (key == -1) { // 失败,终止进程 cerr << "ftok fail! " << "errno: " << errno << " | " << strerror(errno) << endl; exit(1); } return key; } // 共享内存助手 int shmHelper(key_t key, size_t size, int flags) { int shmid = shmget(key, size, flags); if (shmid == -1) { // 失败,终止进程 cerr << "shmget fail! " << "errno: " << errno << " | " << strerror(errno) << endl; exit(2); } return shmid; } // 创建共享内存 int createShm(key_t key, size_t size) { return shmHelper(key, size, IPC_CREAT | IPC_EXCL | mode); } // 获取共享内存 int getShm(key_t key, size_t size) { return shmHelper(key, size, IPC_CREAT); }
processa.cpp
#include <iostream> #include "common.h" using namespace std; int main() { // 服务端创建共享内存 key_t key = getKey(); int shmid = createShm(key, gsize); cout << "getpid(): " << getpid() << endl; cout << "server key: " << toHEX(key) << endl; char *start = (char*)shmat(shmid, NULL, 0); //去关联 if ((void*)start == (void*)-1) { cerr << "shmat fail!" << "errno: " << errno << " | " << strerror(errno) << endl; shmctl(shmid, IPC_RMID, NULL); //即使异常了,也要把共享内存释放 exit(1); } struct shmid_ds buf; int n = shmctl(shmid, IPC_STAT, &buf); if (n == -1) { cerr << "shmctl fail!" << "errno: " << errno << " | " << strerror(errno) << endl; shmctl(shmid, IPC_RMID, NULL); //即使异常了,也要把共享内存释放 exit(1); } cout << "==================" << endl; cout << "buf.shm_cpid: " << buf.shm_cpid << endl; cout << "buf.shm_perm.__key: " << toHEX(buf.shm_perm.__key) << endl; shmdt(start); //去关联 shmctl(shmid, IPC_RMID, NULL); return 0; }
通过程序证明了 共享内存确实有自己的数据结构
结论: 共享内存 = 共享内存的内核数据结构(struct shmid_ds
) + 真正开辟的空间
7.5.用管道解决共享内存没有同步机制的问题
共享内存是没有同步机制的,我们可以借助命名管道来实现同步机制
common.h
#pragma once #include<iostream> #include <unistd.h> #include <sys/ipc.h> #include <sys/shm.h> #include <string> #include <fcntl.h> #include <cerrno> #include<cstdlib> #include<cstring> #include <sys/types.h> #include <sys/stat.h> using namespace std; const string pathname="/home/zs_108/A";//我的processa所在地 const int proj_id=0x7777;//随便取的 const int SIZE=4096;//4kb key_t Getkey()//获取key值 { key_t key=ftok(pathname.c_str(),proj_id); if(key<0) { perror("Getkey failed"); exit(1);//没key玩个屁啊 } printf("key: %x\n", key); //打印key值 return key; } int GetShareMemHelper(int flag)//获取共享内存 { int shmid=shmget(Getkey(),SIZE, flag); //创建新的共享内存 if(shmid<0) { perror("Creat failed"); exit(2);//共享内存都创建不出来,玩什么呢? } printf("shm: %d\n", shmid); //打印句柄 return shmid; } int CreatShm()//负责创建共享内存 { return GetShareMemHelper(IPC_CREAT | IPC_EXCL|0666); } int GetShm()//单纯的使用已经存在的共享内存 { return GetShareMemHelper(IPC_CREAT); } #define FIFO_FILE "./myfifo" #define MODE 0664 enum{ FIFO_CREAT_ERR=1, FIFO_DELETE_ERR, FIFO_OPEN_ERR };
processa.cpp
#include"common.h" int main() { int shmid=CreatShm();//创建共享内存 char* shmaddr = (char*)shmat(shmid, NULL, 0); //关联共享内存 if (shmaddr == (void*)-1){ perror("shmat failed"); return 1; } int n=mkfifo(FIFO_FILE,MODE);//创建管道 if(n==-1) { perror("mkfifo failed:"); exit(FIFO_CREAT_ERR); } //打开管道,等待写入方打开之后,自己才会打开文件,向后执行,open阻塞 int fd=open(FIFO_FILE,O_RDONLY);//读方式 if(fd<0) { perror("open:"); exit(FIFO_OPEN_ERR); } while(true)//通信部分 { char c; ssize_t s=read(fd,&c,1);//管道里面读1个字符,要是没读到就一直break,直到读到一个字符才会执行后面的命令 if(s==0) break; else if(s<0) break; cout<<"processb say@"<<shmaddr<<endl;//直接读取共享内存的内容 sleep(1); } shmdt(shmaddr); //共享内存去关联 shmctl(shmid,IPC_RMID,nullptr);//删除共享内存 close(fd);//关闭管道 int m=unlink(FIFO_FILE);//删除管道 if(m==-1) { perror("unlink :"); exit(FIFO_DELETE_ERR); } }
processb.cpp
#include"common.h" int main() { int shmid=GetShm();//获取共享内存 char* shmaddr = (char*)shmat(shmid, NULL, 0); //关联共享内存 if (shmaddr == (void*)-1){ perror("shmat failed"); return 1; } //打开管道,等待写入方打开之后,自己才会打开文件,向后执行,open阻塞 int fd=open(FIFO_FILE,O_WRONLY);//写方式 if(fd<0) { perror("open:"); exit(FIFO_OPEN_ERR); } while(true)//通信部分 { cout<<"Please Enter@"; fgets(shmaddr,4096,stdin); write(fd,"r",1);//通知对方去读 } shmdt(shmaddr); //共享内存去关联 close(fd);//关闭管道 }
完美啦!!!!
我们这里只是说明共享内存可以实现同步机制,并没有别的意思!!!!