进程间通信之共享内存

共享内存前言

其实我们今天的System V共享内存用的很少的,多线程用的是最多的。包括我们用C++也会用它的那一套机制来进行多线程操作。
但是留下来System V也是挺重要的。

System V是一套标准,这一套标准我们正常进行通信的一套标准。我们代码都是由程序员写的,大家都在用所谓的进程间通信。有很多实验室都在搞,最后谁搞的好,就用谁的标准,最后System V共享内存幸存下来。
对我们来讲它是系统级别的接口,是操作系统原生给我们提供的接口。在我们进行讲之前,先回答一个问题:
进程间通信的本质是?—先让不同的进程,看到同一份资源!
我们之前管道的做法就是,打开同一份文件,让子进程继承同一份文件。但今天共享内存的处理方案就不一样了!

System V原理

我们曾讲过进程的task_struct,紧接着就是对应的进程地址空间mm_struct。task_struct对应的有指向自己的地址空间。我们都知道地址空间里面有划分的代码区、初始化/未初始化数据区、堆区、栈区等。当时也讲了,0~3G是用户空间,3~4G是内核空间等等。
我们现在着重标注出栈区和堆区,其中我们堆区向地址增大方向生长,栈区向下而生,这就叫堆栈相对而生。我们一般从下到上是由低地址到高地址。
曾经我们学过,在动静态库那里,我们可以把所谓的磁盘当中的库load到内存里,然后把我们的库加载到内存之后,通过页表映射进我们的进程地址空间里。我们自己的代码也在地址空间里,当我们执行的时候需要库就跳转到库所在地址空间的地方。这是我们在讲库时说的一个结论,叫做共享库是会加载到内存里,并且映射到我们堆栈之间的区域。
堆栈之间的区域特别大,其中会有一个共享区,其中共享库就在里面。
今天我们再说说。我们还有物理内存。既然是进程间通信,那一定是得有多个进程。比如说是两进程,进程1和进程2。这两个进程是毫无关系的。
这两个进程间呢,每个都有自己的页表。其中他们的区域呢,都能通过页表映射到属于他们自己的物理内存当中。
因为进程具有独立性,本质是因为我们页表的内容呢可以映射到物理内存当中的不同区域。所以我们两个就有了,你有你的区域,我有我的区域,我们是互不干扰的。我们数据结构各自私有,映射关系我们各自维护。我们物理当中对应的物理数据呢,我们两人一人一块。所以我们无论是数据结构层面还是映射关系层面,还是内存的数据和代码层面我们完全没有任何瓜葛。
如果我们今天有一个特定的接口,假设有这么一个特定的接口,它首先第一步能够在我们的物理内存当中创建一份空间,这是第一步。创建好空间之后,第二步,它再把这份空间通过两个进程调用特殊的接口,然后将这个空间呢映射到自己进程的地址空间。比如说我调用了某种调用,然后在这个共享区里面,所以不要只觉得共享区只能放动态库,其他东西也可以映射。所以呢既然共享库可以映射,说白了你共享库如何映射的?不就是把磁盘的数据加载到内存,然后再让操作系统帮我们重新建立映射,然后让我的进程看到你对应的方法。如果现在我干脆就创建好内存,不从磁盘加载。把内存创建好,然后通过我们对应的页表,映射到我们的进程地址空间。然后把这份空间的起始地址,返回给用户,此时这个用户可以通过页笔找到其在物理内存的空间,这是第二步。
那么同样的,你能这么做,我也能这样做呀!如果我也发现这有一个物理内存空间我可以用,我把它通过页表建立映射,映射到我的进程地址空间(当然我和另一个进程映射到地址空间的在我的上下文里的地址可以不一样)。映射完后。再把这个空间的起始地址返回给用户,此时我们各自就完成了。
第一步创建共享内存。第二步,分别把这个共享内存挂接到各自到进程的上下文里面 。既然我只要映射了,进程内部的代码能够跑过去执行库的代码,然后再返回。
既然能执行代码,当然也可以跳转过去访问那块共享内存!访问完毕后再跳转到自己的代码往后面执行!那么其中对我们来讲,这种机制就叫做:共享内存!

所以共享内存是一个非常简单的原理。那么为什么这么简单呢?因为我们之前相关的概念,我们其实都已经有过了曾经走过的路。那么我们回头看呢,就不会很难理解!
所以现在从我们看来,那么我们现在共享内存实际上要的接口,第一个创建一块共享空间的接口,第二个得进行我们对应的映射关联到我们上下文当中接口。除了这种之外,还有一种方式,就是万一我们后面不用了呢?不像管道,不用了就close掉,那你这个共享内存怎么关呢?
除了我们意料之中的除了能创建和能挂接,也一定会存在,我们第一个接口:
创建共享内存 和 删除共享内存
第二个接口:
挂接我们对应的共享内存 和。去关联共享内存
就相当于对我们来讲,最终一定少不了要给我提供至少4个操作!
所以关联内存和去关联内存,在哪做?是谁做?一定是进程在做,OS也有。
那么创建和删除呢?一定是OS内部帮我们做

✳️再稍微总结一下:
我们今天讲的共享内存方式,它呢进程间通信的一种方式。什么通信方式我们也学了, 大概都知道。但一定要记住一句话,以后学到的所有共享内存都是一句话:
叫做,不同的进程要通信,前提条件是,必须得先看到同一份资源,看到同一份资源才能通信。这份资源说直白一点,有可能是文件 ,有可能是我们的内存!所以对我们来讲,必须得看到同一份资源就可以了。
我们前面费那么大劲说,创建共享内存和删除,关联内存等一大堆,不就是看到同一份资源就行了嘛。
当看到同一份资源就完了嘛? 通信才开始,我们只是做了一大堆预备工作,就是为了给通信做准备的。
在这里插入图片描述

共享内存的基本编写

我们要使用共享内存,创建共享内存要用到的接口是:shmget()函数
今天System V版本的接口特别恶心。共享内存是里面最简单的一个,也是最重要的一个,剩下的两个呢,只要一套学懂了,也就问题不大了。
对我们来讲呢,共享内存概念呢,shmget()就是获得共享内存的一个方式,它这个函数对应的一个函数呢就是shmctl()函数。
shmctl()它叫做控制共享内存,可以去进行,比如获取共享内存的属性,设置共享内存的属性,甚至删除共享内存。我们今天重点用的是删除共享内存 。
也就是我们一定有接口,一个用来创建shmget(),一个用来删除stmctl()。
shmat()函数一会儿说,它是用来挂接和关联的;
shmdt()函数就是用来去关联的。
四个接口没有详细介绍,下面呢首先带大家认识的是,上面的那几个我们都不看,首先看一下shmget()函数。

#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

函数介绍:
它就是创建一个,申请一个System V级别的共享内存的。

函数参数介绍:
🌟key:是一个非常重要的概念。我们先讲了另外两个参数,翻了源代码得知key值是要由用户来提供的!
为什么这个key值要由用户提供呢?一切的一切还得是进程间通信的前提是:先让不同的进程,看到同一份资源!
如果我们现在key值由操作系统统一提供,你想想,你调用这个接口,你获得共享内存的时候,给你返回一个key值。
那么另一个进程怎知道的呢?另一个进程是不可能知道的!但是同学们,如果这个key值,能够由一个用户提供,
比如现在有serve和client,现在key值由用户提供,那么serve是不是就可以直接提供一个key值让操作系统帮它创建一个共享内存。
并且我们约定好让client也使用同一个key值,再去进行访问我们的共享内存。因为你serve能够设置,那么我client就可以用了
所以同学们换句话说,我们共享内存现在变成了是用我们的key值来标识我们的共享内存的唯一性,而进程间通信的前提是:
让不同进程看到同一份资源。所以我们可以先有一个结论:
我们共享内存在内核中让不同的进程看到同一份共享内存,
做法是:让他们拥有同一个key即可!!
也就是说不管未来,是你创,还是我创。你创好我获取,我创好你获取。只要我们有一个创建,只要它创建能够通过让它吧key值设置到内核里,
那我也用同样的key值去找的时候,我们两不就可以看到同一份共享内存吗?这不就讲我们两个看到同一份共享资源了吗?

你看函数参数,当你调用shmget()创建共享内存的时候,对应的第一个参数key,那么这个key,他为什么暴露出来的?
一旦暴露出来的,它一定是让用户去用的。所以意思说:你去创建一下吧,你指定一个key值,我到时候在内核里帮你去,以他作为共享内存的标识符!所以呢你怎保证我们两个进程看到的是同一份资源!
很简单我呢创建一个共享内存,我自己用我自己设置好的key,设进去。然后,我们约定好用同一个key。那么此时你也可以用同一个key进行访问了!
这个概念是不是非常像命名管道!你说命名管道是怎么保证两个不相干的进程看到的是同一份资源?
当时说:client和serve约定好创建一个文件,我们用符号名作为公共标识,让我们看到同一份文件。
同样的共享内存它其实说白了就是用户层设置的key值,在创建的时候,就给你设置进去。设置进去之后,我们约定好,
让serve和client用同一个key值。这不就看到同一份资源了吗?
命名管道--->约定使用同一个文件
共享内存---->约定好使用同一个唯一key,来进行通信的!
所以现在重点就变成了:key值我怎么保证唯一性?
理论上自己随便写,只要不和系统内部的不冲突就行!但一般不这么干。讲到这里接口参数就介绍完了。
稍后再回答你怎么知道共享内存存在还是不存在。
🌟size:就是你创建的共享内存的大小。
这个大小一般在创建的时候,建议设置称为页(磁盘和操作系统IO的时候,基本单位是4KB 。
我们从外设磁盘拷贝到内存是按4KB为单位进行拷贝的,
所共享内存大小申请也一定是按4的整数倍)的整数倍。后面会有实验演示

🌟shmflg:首先创建共享内存你就可以理解成操作系统,在物理内存给我们开辟一段空间。
(请问同学们:如果操作系统和我们的磁盘,进行IO的基本单位是4KB,那么我的内存假设是4GB, 
那么我们的4GB内存一定会有多少个4KB的空间呢?4 * 1024(MB) * 1024(KB) * 1024(字节) / 4 = 1048576 。其实是220次方。所以4GB->1048576->220次方。
也就是说,我站在操作系统角度看,看待你物理内存的时候,我把你看待成220次方个页,
1页就是4KB。那么请问操作系统要不要物理内存的这些页,这么多,要不要管理起来呢?
你现在把内存划分成了一块块大小为4KB ,一共有220次方个。肯定要管理!
那怎么管理呢?毫无疑问,先描述,再组织!
那么其中在内核里面,一个页的大小,被称作一个struct page的大小。
struct page
{

}
其中呢,我们操作系统内,可以定义一个struct page[220次方];
所以在操作系统内部,是按数组的方式来把我们的内存管理起来的。相当于有220次方个page,先描述,再组织,就是这么一个数组。
所以我们对内存做管理,就变成了对数组做管理!)

所以我们前面一个参数size设置成4KB的整数倍,比如说4 * 2,说白了就是给你2个页去使用。
下面问题:如果我们现在创建共享内存的时候,是不是就要由操作系统在内部申请物理页,说白了就是一个个struct page结构。
	如果其中当前这个页申请的时候,我们可能会存在这样的情况,就像我们曾经讲命名管道一样:
	1.如果不存在;2.如果存在
	就像命名管道,如果今天要创建一个命名管道通信,我们的server端帮我们创建命名管道文件 ,那么客户端你可以不用管这个文件,直接用就行了。
	所以就相当于我们默认好有serve端统一创建好管道, 创建好让client端用就行了。
	但是未来我们两个要使用同一个共享内存,我们就有可能存在这么一个情况。
	就是这个共享内存由我们两个进程当中哪一个创建呢?如果你创建好了,我该怎么办?
	如果我第一次去创建共享内存,发现底层没有。没有的话,我是自己创建还是怎么样?

所以这里的shmflg,选项的意思,说白了就是。针对于底层不存在,或底层存在共享内存该怎么做。
	常见的有两个选项:
	(说白了我要创建共享内存,就是在内存当中申请一个个page,申请好page之后就属于我了。
我用这个内存读写时,操作系统内部自动会从硬件角度把我的数据写到内存的特定位置,
这是另一码事。现在是申请一个共享内存就是申请page,但是如果我现在,我们两个创建共享内存时一定会存在问题,
共享内存谁来创建?如果我创建,那你怎么知道?我创建好了,它怎么知道它是要新建呢,还是用别人跟他同一个共享内存?所以就有了两个选项)
	1.IPC_CREAT:创建共享内存,就是创建对应的page。如果已经存在,就获取之,不存在就创建之
	(说白了就是如果我用这个选项,单独在调这个函数的时候,它底层内核当中,如果有这么一个共享内存,好吧你就拿走。
如果不存在你就创建它。这就是IPC_CREAT。当然这个选项也可以设置成0,设置成0默认行为就是IPC_CREAT。存在就获取,不存在就创建)

	2.IPC_EXCL:此选项用的时候,是为了确保内存段已经存在的话,要保证你失败。这句话很难理解。首先和大家说一下,我们要用这个选项:
	它通常不单独使用 ,必须和IPC_CREAT配合使用。如何配合呢?
	这两个宏一定是用不同的比特位传递标记为,所以我们用按位或“|”就可以。
	所以含义就是:如果不存在指定的共享内存,就是想访问的不存在, 创建之。
	如果存在了,就是说你要访问的底层的共享内存已经存在了,那就出错返回。所以说IPC_EXCL的返回值就是错误值,返回-1和设置errno的值。
	出错返回的意义在于:可以保证,如果shmget函数调用成功---一定是一个全新的共享内存!
	(什么意思呢?你现在可以获取成功,但是成功了,我不知道到底是一个你自己创建的呢
还是你把别人拿过来的?如果我将这两个选项合起来使用吗,如果一块传,只要这个函数调用成功了
那么得到的一定是一个全新的共享内存。因为如果调用,底层是存在的话我会出错返回的。)

所有的这一切都有一个前提条件(你说我调用什么选项,然后创建共享内存,操作系统层面上
描述页呢是用page来描述的。反正就是共享内存给你创建好 ,
创建好之后呢,这个page是有id的,然后用的是哪个page我也知道。
现在最关键的问题是。你说如果存在了怎么样...不存在怎么样。。。。又说这两个选项配合,你又是存在了怎么样...不存在怎么样。。。。
那么我的问题就是:上面的所有这些有一公共前提):
1.存在哪儿?-----在内核中---内核会给我们维护共享内存的结构。(你们两个进程间可以通信,他们两进程间也可以通信 。
系统里面存在了几十几百个进程都在通信。既然你操作系统要帮我维护共享内存的话。通信的进程太多了,
操作系统要不要把每一个共享内存也管理起来呢?相当于操作系统内,是你操作系统给我提供的共享内存的机制 。
进程是进程,两个父子进程或是两个任意不相关的进程。两个进程间要通信,是你内核给我维护的通信方式,其中有十几对要通信,
有十几个共享内存在系统里面存在,那么你操作系统要不要帮我管理这些共享内存呢?操作系统在内核当中,有多对进程,
一定会存在很多共享内存。只要用共享内存的话,就会同时存在多个。那么操作系统你要不要管理呢?比如说一会儿删除,一会儿释放,包括查找共享内存,确定用户所传入的共享内存是否存在,这些等等都是要操作系统要做的)

所以共享内存也要被管理起来,那么就是先描述,再组织!
也就是说内核当中一定会存在,描述共享内存的数据结构!
比如struct ipc_shm
{
	//各种属性(容量、权限、对应的物理内存块即page在哪、id)
}
也就是我将来存在大量共享内存,这些共享内存一旦创建好后,每个共享内存都必须有自己的id值!
我们在翻内核代码时,翻到了我们还没讲的key参数。所以意识到,共享内存要被操作系统管理,所以共享内存在内核里面有自己的对应的数据结构。
它的数据结构里面有一个结构内部是包含了共享内存的key!这就是共享内存的唯一值!

因为我们共享内存要被管理-->struct shmid_ds{}-->struct ipc_perm--->key(shmget)(它就是共享内存的唯一值!)
我们后面会谈为什么这个key值要由用户提供!

所以说共享内存现在是存在具有唯一值这么一个特性,说白了就是一个标识这块内存的唯一性的!
所以就能回答这个共享内存存在还是不是存在。所以我们要知道这个共享内存存在还是不存在,你是不是得有方法标识共享内存的唯一性。
先有方法标识共享内存的唯一性!!若共享内存在系统中是否唯一都做不到,你怎么知道你对应的共享内存在底层是否存在呢?
所以先有方法标定唯一性就是由我们刚刚在源代码里面看到的key确定的!

现在我们知道共享内存有对应的数据结构,它的数据结构内部包含了这么一个key值,这个key值保证了它的唯一性。
所以呢对我们来讲,未来就具备了能够知道他有没有被创建的前提。你必须得先标定唯一性才知道你到底是存在还是不存在对不对。
下面再来回答第二个问题:这个key值是要由用户提供的!那为什么呢?见key参数解释,即翻到上面
2.我怎么知道,这个共享内存属于存在还是不存在
(就是你刚刚假设了,比如说我在调它的时候,共享内存曾经人家创建好了,我再去创建。比如去获取,或创建的时候发现出错了,那么你怎么知道我们的共享内存是存在还是不存在呢?)

返回值介绍:
当我们创建了共享内存,指定了key,指定了大小,指定了选项,操作系统就帮你创建了。创建好成功,那么就是我们对应的一个整数,
它是一个合法的共享内存的标识符被返回,失败就会返回-1,这东西特别像一个文件描述符东西。

我们的key一般不会很傻的自己拿着key去做相关操作。那么这个key值我们怎么拿到呢?用ftok()这个函数做生成

ftok()函数
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
函数介绍:
找到特定的文件,然后结合你自己设定一个值,一般0255就可以了。你要设置的这个值呢,设置好后,在内部会把你传的pathname文件的inode,它是唯一的,
然后和你指定的proj_id两个数字做组合,形成一个唯一值返回给我们的key。
不需要关心细节,里面是数学设计,帮我们构建具有唯一性的数字就可以了。
因为pathname的文件的inode是唯一值,再加上你传的proj_id你们约定好形成key值!
用它设计好保证key值的唯一性,换句话说,在我们共享内存里面呢,只要key是唯一的就可以,至于key是多少不重要!
只要成功了这个key值就会返回,失败了-1就会被设置
这个就是ftok的做法。
Makefile

.PHONY:all
all: IpcShmCli IpcShmSer

IpcShmCli:IpcShmCli.cc
	g++ -o $@ $^ -std=c++11
IpcShmSer:IpcShmSer.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f IpcShmCli IpcShmSer

我们第一件事情,不是创建共享内存,而是先获取key值!并且这个key必须得是我们双方达成共识。得约定好用同一个key。
那怎么办?
我们创建Comm.hpp,在其里面定义一个路径给将来的ftok()的参数pathname使用。然后第二个参数我们自己定义为0x66
然后IPCShmCli.cc和IpcShmSer.cc都包含Comm.hpp,此时就使用了同一个约定好的同样的PATH_NAME和PROJ_ID。
在Comm.hpp写创建key值的函数。
有了key值就要创建共享内存了,就要用到shmget()函数了。
当我们IpcShmSer创建共享内存成功之后,然后在命令行多次执行./IpcShmSer,会发现第一次成功之后,我们继续./IpcShmSer执行程序会创建失败!那请问IpcShmSer进程你认为退出了吗?
早就退出了!因为它早就将自己的代码跑完了。
所以我们发现当我们运行完毕创建全新的共享内存的代码后(进程退出了),但是第二次(n)次的时候,该代码无法运行,告诉我们file存在!也就是你要创建的共享内存存在。
不对呀,首先我创建共享内存,它第一次是创建成功的,创建成功后,当我进行第二次创建时,我之前的这个进程退出了,所以共享内存是不是和文件一样,进程退出那么共享内存就会被释放呢?
经过实验一般我们打开文件,进程退出了,那么这个文件也就退出了,刷新缓冲区,文件自动关闭什么的,引用计数减到0操作系统将其关闭。
你的进程退出了,共享内存还在吗?还在!因为们第二次调用的同样的代码和选项。第一次调不存在,就创建成功了。第二次我再调的时候,不好意思,它依旧用的IPC_CREAT和IPC_EXCL的选项其中IPC_EXCL意思是如果存在就报错。那么很显然第二次 ,再进行运行./IpcShmSer,它的报错码告诉我们已经存在。
有人会说,你的进程都退出了,你的共享内存资源难道还不释放吗?答案:是的!最终结论就是 :
system V下的共享内存,生命周期是随内核的!!
说人话就是,如果不显示的删除,那么这个共享内存只能通过kernel(OS)重启来解决!
难道他和new与malloc一样吗?当我们进程退出的时候,我们malloc和new出来的空间,操作系统会自动回收和释放!与共享内存不一样!
那我怎知道我的有哪些IPC资源?刚刚是代码验证,现在我得证明这个IPC资源是存在的,这是第一。
#ipcs -m命令用来专门查看当前用户曾经创建的共享内存。查看之后看到了我们第一次创建的共享内存确实存在!
所以当我们再次运行程序的时候会说创建共享内存失败。
第二如何显示删除呢?
第一种删除共享内存的方式:#ipcrm -m ➕shmid值(shmget()返回值标识共享内存的)
这个太扯了吧,我们每次用共享内存还得拿命令删除,万一我是写代码写了个服务器,服务器用了共享内存,不是扯淡吗?我写完跑好后,我还得亲自拿命令删除,肯定不行!
还可以用系统接口来进行删除!
系统接口shmctl()

shmctl()函数
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

函数参数解释:
🌟shmid:不就是曾经创建共享内存给我们返回的值吗?代表你要对哪个共享内存做操作。

🌟cmd:你想对共享内存做什么操作?如果是删除就传入IPC_RMID,然后第三个参数buf你设置为NULL就行了

🌟buf:

我们申请共享内存的大小是以4KB为单位的,如果你申请4097,实际中,操作系统内部会多给你2个page8KB,可是我们用#ipcs -m命令查看到就是4097呀。虽然操作系统多给你申请了,但是你只能用4097的大小。
我们上面做了一大堆工作,也就只吧创建共享内存和删除共享内存的工作做完了。那怎么用呢?
那你首先做的就是挂接!就得把你创建的共享内存和你的进程关联起来。
共享内存虽说你创建的,但并不属于你!(我们农民工盖的房子是他们盖,但是房子不属于他们)你把你共享内存创建好了,但共享内存是属于操作系统的,所以你这个进程退出了,操作系统说你退吧,共享内存我给你保留着,我帮你照顾好,这个共享内存不属于你!
你将共享内存创建出来了,只能说你创建了它,但并不能说是属于你。
所以你要用它,就必须得做第二个工作,要将我们的共享内存和自己的进程产生关联。所以就用到接口shmat()接口,at就是attch联系,附在...上单词的简写。
既然谈到关联shmat(),那就不得不谈到shmdt()去关联,dt就是detach拆卸单词的简写。
shmat()函数/shmdt()函数
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
函数参数解释:
🌟第一个参数 shmid:你要关联哪一个共享内存

🌟第二个参数 shmaddr:如果你把共享内存挂接到你的地址空间里面,你想把它挂接到空间地址什么位置上?该参数一般是特殊用途,我们今天用不到,设置为NULL就行

🌟第三个参数 shmflg:正如你吧共享内存挂接上,可是你对应的是把共享内存是读呢?还是写呢?我们当然是读写。
所以默认填0就可以挂接上了。

总结:所以第一个参数是你要挂接哪个共享内存,第二个参数设置为NULL。第三个参数设置为0就行了

返回值介绍:
如果成功了就会返回你挂接的共享内存的地址,出错就会返回-1。
malloc返回值是void*的意味着,将来你怎么用malloc的空间,你就怎么用共享内存的空间。肯定是不能free的哈。
所以就将其返回值想象成malloc的返回值,你想当成整型就强转成整形,想把它当成什么类型,强转就完了!
----------------------------------------------------------------------
shmdt()函数
int shmdt(const void *shmaddr);
函数参数解释:
🌟第一个参数 shmaddr:就是shmat()函数返回的地址。
也就是说将来你要去关联,说白了不就是修改页表吗,修改页表不就是说,把曾经建立好的映射关系去掉吗?说白了就是
只要知道虚拟地址的起始地址加我的共享内存长度我就可以把这个映射关系从我们页表当中消掉,此时该进程就不会和共享内存又关了。

如果我今天调用了shmat()把我的共享内存挂接到我对应进程的上下文里。是不是我内存凭空多了一块内存空间?多了这么一份内存空间,我们后续是不是可以用这份空间了。那么如何去理解shmat()的返回值。我们在malloc 的时候,说白了就是你自己在内存申请一段空间,然后通过页表映射到你的地址空间的堆区。所以malloc返回值是void*的意味着,将来你怎么用malloc的空间,你就怎么用共享内存的空间。肯定是不能free的哈。
所以就将其返回值想象成malloc的返回值,你想当成整型就强转成整形,想把它当成什么类型,强转就完了!
当明白返回值后,要产生关联,怎么产生关联呢?我们想把它当成字符串来看待,想将其当成大空间,一个字符一个字符去访问。那怎么做呢?强转就完了。
我们现在已经能够共享内存挂接到我们的地址空间了,其起始地址以shmat()函数的返回值交给我们 。有同学会问,那大小呢?大小是你前面就定好了!所以就相当于往后,你拿着空间的起始地址,以及地址空间的长度访问这个空间了。
如果我们想用共享内存,那肯定是要关联,用完之后怎么办?那肯定是去关联。也就是我们把当前进程和共享内存之间关系去掉,调用系统接口shmdt()函数接口,上面和shmat一起展示了。
共享内存不是你client端创建的,所以你就不需要去做删除共享内存的操作了。

所以遗留的问题:你怎么知道共享内存存在还是不存在呢?
作为我们对应的serve端它创建好共享内存后,用的是key值,另一端也是用的相同key值。所以在创建的时候,在内核当中创建好共享内存对象。然后呢,你对象在用的时候,也要传入key值。此时我们就可以拿曾经设置好的key值和你将来获取的key值做对比,来确认是否存在!这个问题就搞定了。
所以我们前面做了这么一大堆事情,我们通信了吗?还没有。我们都是为了完成通信的前提:先让不同的进程,看到同一份资源!现在访问到同一个共享内存了。
接下来我们就要用共享内存了。我们有没有用系统调用向共享内存写入?注意!我们接下来要讲的是用共享内存的,那么我们前面把共享内存创建出来了。我们以前把管道无论是匿名还是命名,我们将管道创建好之后,我们是不是可以直接用管道。
但是当时可不是向我们现在这样用的,str[cnt] = ‘A’ + cnt,这样。当时可使用文件操作read、write等接口向管道写入。
今天我们用共享内存,竟然没有使用任何的系统调用接口。我们是直接向们的str里面写入的。
这个str空间,还记不记得我们曾经讲地址空间的时候说过:从命令行参数和环境变量到代码区,我们叫做用户空间。在其上面的是内核空间。
我们把共享内存是映射到进程的空间的用户空间的堆栈之间,(这个共享内存我们创建好了,我们是吧这个共享内存挂接到映射到我们的堆栈之间。堆和栈都是用户的,堆栈之间更是用户的!) 换句话说,对每一个进程而言挂接到自己的上下文中的共享内存,属于自己的空间,类似于堆空间和栈空间,可以被用户直接使用!!
这里我们对应的被用户直接使用,说人话就是,你根本不需要调用系统接口!!就好比用你自己的堆空间和栈空间。你以前自己定义变量想要用的时候入int cnt = 0;你会调用系统接口吗?不会。这是用户直接使用的。
我是一个client端,我向共享内存里面写了一部分内容。那么其中我们的server端获得创建了key值,启动起来,让我们去写,接下来用它。我有个小问题:对于我们clinet端是可以直接使用,对于server端也是把共享内存挂接到我们自己的用户上下文中的堆栈之间,你client端可以直接写,那我的server端是不是可以直接 用共享内存里面的数据?换句话说我们server端要用它,那你就用呗,
那么我们接下来就让我们的server端做一个小小的工作,让他循环打印共享内存空间。当client端没有去写的时候,请问他有没有卡在那里等写入再读?
没有管别人!反正这共享内存是我的,我不知道client,我们各自不知道对方。当我们client执行后去向共享内存写入,server端立马就能获取到将其打印出来。
这就叫做client端将数据写入到了server端。
我们又发现一个现象,server端并不会因为你共享内存里面有数据还是没数据而停下来。比如说空的时候,它不会阻塞,等你写入了再拿去打印。
告诉大家:因为共享内存的自身特性,就决定了它没有任何访问控制。也就是共享内存被双方直接看到,属于双方的用户空间,可以直接通信,但是不安全。
所以共享内存这种通信特征呢,说白了就是我们一旦共享内存创建好了,只要映射到我们的地址空间,这里我能看到,它也能看到。只要任何一个进程,向共享内存里面写入,对方立马就看到。也就是说这段空间即属于你,又属于别人。
挂接的本质就是将共享内存临时性属于你们两个。属于你也属于他。但它并不知道两个的存在,同时两个人也不知道互相的存在。我们两个都可以零成本的去使用共享内存。所以我这里一旦向共享内存里写入,它就立马能看到。
进程间通信之共享内存,是所有进程间通信,速度是最快的!
相比于管道,有两个进程。你将管道创建好。首先你得将数据拷贝到缓冲区里面,调的系统接口write()是一次拷贝了。然后你还要再进行,read()将管道里main的数据拷贝到你的空间的里,这里又是一次拷贝。所以单单在这就已经拷贝了两次。
但是共享内存这种模式,我们两拥有同一份共享内存,你的一个进程想发一个数据,它数据是从文件或者用户键盘输入,它天然的只要把数据写入到共享内存里面,对方立马就能看到。这就是共享内存的概念。所以它通信速度是最快的。
为什么最快的原因就是,如果你从外设拷贝,它是从外设里面直接拷贝到你们的共享内存里面,直接一步到位,双方就能看到。拷贝的次数少了很多。
如果不在已经写好的共享内存的代码里,加上管道的策略来控制访问,那么其中对我们来讲,我们client端刚写什么,serve端就立马读取了。当我client端认为aaabbb是个整体,写入到共享内存的时候,我可能只写了一半就被你读了!
我要你aaabbb一块儿读取到才能处理出正确结果。但我刚给你写一半就拿走了,那么你对数据处理加工就可能出现错误。
那么就要引入一个概念!我写在下面了。

Comm.hpp
#define PATH_NAME "/home/wh/104"
#define PROJ_ID 0x66
#define SIZE 4096
上面的两个都是给ftok()函数的参数 

key_t CreateKey()
{
    key_t key = ftok(PATH_NAME, PROJ_ID); ---➡️成功返回key值,失败返回-1
    if(key < 0)
    {
        std::cerr <<"ftok: "<< strerror(errno) << std::endl;
        exit(1);--➡️创建key失败了,你就别往后走了,key都没通什么信。
    }
    return key;
}

void CreateFifo()
{
    umask(0);
    if(mkfifo(FIFO_FILE, 0666) < 0)
    {
        Log() << strerror(errno) << "\n";
        exit(2);
    }
}

----------------------------------------------------------------------
下面的是想共享内存和管道配合使用
#define READER O_RDONLY
#define WRITER O_WRONLY

int Open(const std::string &filename, int flags)
{ 
    return open(filename.c_str(), flags);
}

int Wait(int fd)
{
    uint32_t values = 0;
    ssize_t s = read(fd, &values, sizeof(values));
    return s;
}

int Signal(int fd)-----通知你可以来读了
{
    uint32_t cmd = 1;
    write(fd, &cmd, sizeof(cmd));----将来进程通过读取阻塞在那里,然后我想通知他数据有没有好,通过write()一旦写入就醒过来了
}

int Close(int fd, const std::string filename)
{
    close(fd);
    unlink(filename.c_str());-----删除指定的文件
}

log.hpp

#pragma once

#include <iostream>
#include <ctime>

写了一个简单的Debug信息
std::ostream &Log()
{
    std::cout << "Fot Debug |" << " timestamp: " << (uint64_t)time(nullptr) << " | ";
    return std::cout;
}

IPCShmCli.cc

#include "Comm.hpp"
#include "Log.hpp"
using namespace std;
// 充当使用共享内存的角色
int main()
{
    //int fd = Open(FIFO_FILE, WRITER);-----以写的方式打开
    
    // 创建相同的key值
    key_t key = CreateKey();
    Log() << "key: " << key << "\n";

    // 获取共享内存
    int shmid = shmget(key, MEM_SIZE, IPC_CREAT);----➡️不需要有IPC_EXCL了,我们已经让serve端去检查了,若还有就会造成程序错误哈,因为IPC_EXCL会检测到已经创建了,就会报错。
    if (shmid < 0)
    {
        Log() << "shmget: " << strerror(errno) << "\n";
        return 2;
    }

    // 挂接
    char *str = (char*)shmat(shmid, nullptr, 0);

    // 用它
    // sleep(5);
    // 竟然没有使用任何的系统调用接口!
    // str 
    while(true)
    {
        printf("Please Enter# ");
        fflush(stdout);

		//我们要从我们的键盘里面读取,将其读到我们的str里面。就是你要写的东西写到str里面,直接干到共享内存里,期望读  多少?就是你共享内存大小!
        ssize_t s = read(0, str, MEM_SIZE);
        if(s > 0)
        {
            str[s] = '\0';
        }
        Signal(fd);
    }
    // int cnt = 0;
    // while(cnt <= 26)
    // {
    //     str[cnt] = 'A' + cnt;
    //     ++cnt;
    //     str[cnt] = '\0';
    //     sleep(5);
    // }

    // 去关联
    shmdt(str);

    return 0;
}

IpcShmSer.cc

#include "Comm.hpp"
#include "Log.hpp"

#include <unistd.h>

using namespace std;

// 我想创建全新的共享内存
const int flags = IPC_CREAT | IPC_EXCL;
----➡️这两个选项同时使用,是不是就是对应的我们刚刚所说上面的flags,它两同时使用,如果不存在就创建之,如果存在就出错返回

// 充当使用共享内存的角色
int main()
{
	//CreatFifo();-----让server端创建命名管道
	//int fd = Open(FIFO_FILE, READER)-----打开管道的读端
	//assert(fd > 0);
    key_t key = CreateKey();
    Log() << "key: " << key << "\n";
	
	//serve创建共享内存 
    Log() << "create share memory begin\n";
    int shmid = shmget(key, MEM_SIZE, flags | 0666);---➡️还需要按位或“|”上共享内存的权限,这样是允许的!
    if (shmid < 0)----➡️创建共享内存失败
    {
        Log() << "shmget: " << strerror(errno) << "\n";
        return 2;
    }
    Log() << "create shm success, shmid: " << shmid << "\n";
    // sleep(5);

    // 1. 将共享内存和自己的进程产生关联attach
    char *str = (char *)shmat(shmid, nullptr, 0);---➡️将其共享内存空间看待成字符串来控制,所以强转成char*
    Log() << "attach shm : " << shmid << " success\n";

    // sleep(5);
    // 用它   
    while(true) //我client作为写的一方,我还没有给你写消息,那就不要动,serve管道已经给大家创建好了,我没写消息你serve别打印,所以你要wait
    {
        // 让读端进行等待
        if(Wait(fd) <= 0) break; 
        printf("%s\n", str);
        sleep(1);
    }

    // 2. 去关联
    shmdt(str);
    Log() << "detach shm : " << shmid << " success\n";
    // sleep(5);

    // 删它,删除共享内存
    shmctl(shmid, IPC_RMID, nullptr);

    Log() << "delete shm : " << shmid << " success\n";
    //Close()---关掉管道
    return 0;
}

在这里插入图片描述
在这里插入图片描述

✳️我们引入概念:被多个进程能够看到的资源------“临界资源”!
如果没有对临界资源进行任何保护,对于临界资源的访问,双方进程在进行访问的时候,就都是乱序的,可能会因为读写交叉而导致的各种 乱码 废弃数据 访问控制方面的问题!!

说白了就是:如果两个进程,他们看到一份共同的公共资源,但没有任何保护,目前我们有的经验是管道本身就提供了访问机制。第二个我们自己也在共享内存代码中添加管道来进行保护。若都不提供,而是裸的共享内存,它被双方同时看到,那么此时我们在进行操作的时候,就会出现乱码问题。
最典型的就是父子进程各自printf,你打你的,我打我的各不谦让。此时对我们两,两个进程同时看到的一份资源是显示器资源。
又比如,如果双方在进行正常通信的时候,因为没有任何保护,共享内存是两个进程都能看到的。那么此时,进程A往共享内存里面写,它并不知道,另一端进程B来读。进程B什么时候来读?读几个字节?这完完全全都是看命的。如果进程A认为要写入1+1 = 2的完整字符串,那么进程B要读取完整1 + 1 =2 才能进行后续处理。如果刚输入一半就被读取,那就会出问题。

❓那么管道是临界资源吗?我们说能被多个进程看到的资源就是临界资源,我们并没有说临界资源是不安全的。管道是的!临界资源有安全访问和不安全访问。

✳️对多个进程而言,访问临界资源的代码称之为—“临界区”!!
我们上面写了一大串代码,是不是都在访问临界资源?不是的!
我的进程代码中有大量代码,只有一部分会访问临界资源,其中这些代码就被叫做临界区!

下面我们用共享内存将这两个概念套一套:当我们创建好共享内存的时候,对于这两个进程而言,它们看到了同一个共享内存,所以共享内存是它们的临界资源。其中一个进程往共享内存里面写,一个往共享内存里面读。那么他们两个分别对共享内存做读写的那部分代码叫做它们两个的“临界区”
一会儿会讲怎么去理解对临界资源做保护。其实呢我们后面的锁现在一个都没学,多进程现在也不管她,多线程后面会重点谈。但是有两个概念一会儿会介绍一下。介绍完后,我们正式进入信号量的话题!

你说我们进程间通信,未来要是把网络一学,那我们的消息是不是张三的消息通过进程,传递给对应的李四的进程。然后把消息推送给李四,那么其中远程就可以加入我们聊天了。
那么我们现在缺的是网络,网络现在搞不定,所以双方想要进行远程通信成本会很高。

临界资源其实对我们来讲多个进程都能看到的资源,临界资源有安全的也有不安全的,主要取决于它内部有没有自我保护。像共享内存这东西,他自己没有任何保护机制,需要你再写代码来保护,我们刚刚是用管道来保护的。

✳️我们把一件事情要么不做,要么做完了-----称作“原子性”。
原子性我们还接触不到,那么我现在告诉大家,一件事情要么做了,要么不做,要做就做完,这种工作就叫做原子性。它直接的表现就是我们没有中间状态!我们将其称作原子性。现在不想做解释,后面讲信号量的时候,结合信号量给大家做理解。

我们的临界资源要做保护,我们有一个概念叫做:
✳️任何时刻只允许一个进程进入临界资源,我们把这种特性叫做:“互斥”。
比如说:现在自习室,一次只允许一个人自习。当我进入的时候,其他人就不能进来,不能进来的时候,这个自习室对我来讲就是互斥的。相当于在任何时刻,只允许一个人来自习室自习,这种特性我们叫做“互斥”

好了,我们引入的概念“临界资源”、“临界区”、“原子性”、“互斥”已经说了。准备工作做好,总结一下共享内存,就进入下一个话题:信号量!

在这里插入图片描述
2159116725
2174033838

共享内存的总结

共享内存是我们进程通信之间,速度最快的一个。它呢可以直接通过,让共享内存映射到进程进程地址空间,此时一方做修改,另一方立马能看到,这就是共享内存。
其实呢共享内存是操作系统级别给我们提供的一个通信策略。代码已经在上面写了。
其中共享内存优点就是快,缺点就是没有访问控制。
共享内存没有保护机制,它肯定不是互斥。因为当client写入的时候,我们的serve同时就来读了。比如说我们要a到z写完你才能读,但实际上你写一个它就读了一个。
System V里面还有一种消息队列 ,可以采用#ipcs -q去查看。创建消息队列呢是msgget(),其里面的key和我们说的一样,然后msgflg和我们上面的也是一样的。返回值是消息队列的标识符。msgctl()就可以对消息队列作删除;这些已经很陈旧了,我们说的共享内存也用的少。还有System V信号量也差不多。
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值