system V版本的共享内存

在之前的学习中我们学习到了使用匿名和命名管道进行进程间的通信,下面我们再来使用一种新的方式进行进程间的通信。

我们下面要学习的是system V版本的共享内存。

首先我们要知道什么是system V

首先我们要知道我们在之前学习的管道通信的代码并不是一个专门设计出来的模块,我们之前所学习的管道通信是复用了文件系统模块的代码的。

但是我们在实际的使用场景上来说,只有一种通信方式是不够的。所以我们还是需要有其它的通信方式存在。

由此就有人设计了一个专门用于进程通信的模块。我们下面要学习的system v版本的共享内存,以及消息队列和信号量都是有人专门设计出来的os内核模块用于进程间通信。既然是单独设计出来的模块那么肯定需要存在的就是一个标准(接口函数的使用,命名,参数的意义)。

标准存在的意义

那么为什么要存在这个标准呢?

在我们计算机的领域中标准是很常见的,首先我们要知道如果一件事情,一个人就能够完成,那么还需要标准吗?答案是不需要的。如果一件事情我们一个人就能够完成那么我们是不需要标准的。所以标准的前提条件就是一件事情需要我们多个人一起去完成才需要标准的存在。例如我们在学校中交作业的时候,每一个作业,老师都是有要求的,而这个要求不就是老师制定的标准吗?因为交作业这件事情需要我们每一个人(学生)都去做,所以就需要标准的存在,在互联网的领域标准可以说是经常见到的。工厂生产制作的时候也是1具有标准的。而在我们的互联网领域一个稍大的开发,就需要多人协助,所以需要标准,这个标准制作出来,不是让我们单独一个人去遵守的,而是要让大家去遵守的。由此我们就知道了其一:在人多的时候是需要标准的。

其二:在现在的互联网时代,我们几乎每一个人都被互联网连接起来了,除了抖音和游戏之外,我的天选电脑,和别人的华硕电脑都是能够访问同一种互联网的。不要忘了,我们组装一台电脑的软件,硬件,os,都有可能是不同品牌的。甚至于有的品牌之间还是竞争对手,那么怎么可能会出现相互通信呢?这就是因为不同的品牌之间遵守了相同的标准,因此不同品牌的电脑在某些方面也是能够做到协同的。这样不同品牌做出来的东西之间才能够实现互相的通信,才能给予用户更好的体验。用户体验好了,才会有更多的用户去使用某个品牌,由此实现了双赢。

所以在这里就需要有人去制定标准。

那么到了第三点,这个标准由谁去制定呢?

在制定标准的时候,标准是偏袒于任何一方都是不可行的。并且制定出的这个标准一定是业界领先的一个标准。以上是制定标准的一个基本的指标。由此我们能够知道,定制标准是一个权力非常大并且很重要的一件事情。

我们就不再深入了解制定标准的事情了。但是当制定了标准之后,其余人只要遵守这个标准,那么不同的公司/个人就能够写出相似/能够互相通信/协作的软件。

总结:标准比较常见,现在也存在的一些的说法就是一流的公司做标准,二流的公司做产品,三流公司做技术。后面的两个逻辑一直在变,但是第一个逻辑是一直不会变的。以上我们一直没有提我们接下来要学习的system V版本的共享内存,以上只是为了让我们能够理解标准的合理性。对于我们每一个人而言,标准定好了,对于我们每一个人都是具有好处的。对于商业公司而言也是能够获得更多的利益的。也就是标准是非常重要的,对于两家公司而言,如果一家公司遵守了标准获得了利益,但是另一家没有遵守那么获得的利益中是没有这家公司份的。所以对于大公司的工程师而言,如果存在制定标准的可能,是一定会去争取的。所以制定标准是一件很重要的事情,但是制定了好的标准对于每一个遵守这个标准的人而言都是具有好处的。

下面我们就来了解一下system V标准。

了解system V标准

首先我们要知道在一台计算机中是存在很多种不同的通信场景的,例如:有的通信是以传输数据为目的的,有的是以快速传送数据为目的的,有的是以传送特定的数据块为目的的,有的是用来进程之间协同和控制的。总之就是不管出于什么目的都是存在要让不同进程之间通信的情况的。也就是让进程之间进行通信是存在多种场景的。如果每一种进行进程间通信的情况都有自己的接口和返回值,那么这对于大家都是不好的,所以就存在了将系统种所有的通信场景聚集到了一块。然后将所有的通信的接口进行了统一(命名,返回值参数,底层原理)。此时我们整体打个包,我们就叫这个整体为进程间通信。此时在Linux领域前沿的人。就有人制定了对应的标准,然后又有研发团队将这些限于本主机内通信的模式实现出来,并且将其命名为system V。

也就是说在我们的Linux中,system V标准的进程间通信时存在多种的,例如消息队列,共享内存,信号量等等。他们在接口和内核数据结构的解决方案上也是比较相似的(标准统一)。以上就是我们需要了解的system V的基本的知识。

下面我们就来学习第一种共享内存。

共享内存

我们通过以下几个步骤来理解共享内存。

共享内存的原理

首先我们要知道一句话:只要是进程间通信那么就一定会具有一个前提:必须让不同的进程看到同一份资源(必须由os提供)。

我们之前学习的命名和匿名管道也都是具有这个前提的。

所以我们接下来学习的这个共享内存,首先肯定也是需要忙活半天就为了让两个不同的进程看到同一份资源。

虽然现在我们已经知道了通信方式存在很多种,但是我们都还没有开始学,只学过匿名和命名管道两种,共享内存,消息队列等等都是没有学过的。但是我们心中要很清楚,接下来的这个共享内存肯定要忙活半天就只为了一件事情,那就是要让不同的进程看到同一份资源,同时消息队列和信号量也是这样的,只不过三种模式在底层实现的原理上有些许的不同。

那么怎么做的呢?

首先我们来看一个最为常见的图:

以上是一个进程结构体自己的虚拟地址空间,以及经过页表映射之后实际在物理内存中位置的图像。我们在之前的学习中我们知道,os是存在缺页中断的机制的,也就是当我们的一个进程在申请空间的时候,虚拟地址空间就会将一个虚拟地址直接给与给这个进程,当后面这个进程要使用或者虚拟地址的空间的时候,如果经过页表映射发现没有在物理内存申请空间,就会发生缺页中断。也就是我们的os具有直接在物理内存上申请空间的权限。

那么能不能这么做呢?

首先os在物理内存上申请一片空间。

然后我们的两个进程不是要通信吗?

然后我们将这个地址空间经过对应的进程页表映射之后,选择虚拟地址中堆和栈之间的某个位置,申请虚拟地址。

我们能够理解先在物理内存中开辟空间,然后映射到这个虚拟地址空间的操作,因为这个操作和动态库加载的过程是一模一样的,不同的点无非就是动态库加载到物理内存中是有内容的,而现在这里由os开辟的这个空间是不存在任何内容的。我们动态库都能完成这样的操作,现在这里一样是能够完成的。(先在内存中开辟空间,再建立到对应进程虚拟地址空间的堆栈之间某位置的映射,最后将这个虚拟地址返回给上层)。此时我们在上层直接使用起始虚拟地址,我们写入的数据也就写入到这个新开的空间中了。那么为什么我们使用这个虚拟地址就能够完成对这个新空间的写入呢?这里我们就要来看下面的这张图:

我们可以看到虚拟地址空间是分成了两个区域的,一个是用户空间,一个是内核空间,虽然我们现在还不知道什么是用户空间什么是内核空间。但是这里我们需要知道的是,我们曾经建立的管道/文件之类的东西他是属于内核数据结构的。那么所有的缓冲区和文件属性都是在地址空间的内核区域的。所以我们需要访问它就需要调用系统调用。那么为什么调用系统调用就能够访问呢?我们在后面讲解信号的时候会说明。但是现在我们发现堆栈之间包括这个共享区都是属于用户空间的。也就是只要我们将这个共享内存建立好了,之后访问这个共享内存的时候是不需要调用系统调用的。直接就相当于我们的进程自己malloc了一片空间。

我们只需要知道,只要os将这片空间申请完成并且映射到了某个进程的堆栈之间,我们就可以认为这个进程拥有了一片新的空间。对于另外一个进程而言也是一样的。

这两个进程得到的虚拟地址(start_addr)可以是一样的也可以是不一样的。

自此我们就通过了共享内存的原理达到了进程间通信的一个前提。让不同的进程看到同一份资源。这就叫做共享内存。以上也就是共享内存的原理。申请空间再映射到不同的进程中。

也就是说现在我们的共享内存总共分成两个大的步骤:第一个os申请空间,第二个将对应的空间挂接到不同的进程上。

那么如果我们要释放共享内存要怎么做呢?

首先我们肯定要取消进程和物理内存的映射这一步,这一步无非就是将页表中对应的映射清理了即可。当页表被清理了之后此时的进程和对应的物理内存也就不再映射了。这里我们也就能知道我们虚拟地址空间中的地址需要真正的具有意义,都是需要建立在页表的基础上的。一页表中的内容被清除了,那么这里虚拟到物理的映射关系也就失效了。所以我们建立映射无非就是将页表填充好,然后将虚拟空间中的起始地址返回即可。这里也就能够帮助我们额外理解一个概念。这个概念就是我们在malloc和new空间的时候,我们经常说是在地址空间中申请实际上是在哪里申请呢?实际上是在页表当中申请的。

即使一开始没有在物理内存中开辟对应的空间,但是只要是在页表中申请了对应的映射内容的空间,然后映射到虚拟地址空间中,然后将虚拟地址的开头地址返回给上层,那么就相当于这个空间已经被开辟成功了。所以所谓的在虚拟地址空间中开辟起始就是在页表当中进行开辟空间。再将页表当中的映射关系清楚之后,最后我们再从物理内存中释放那一片空间完成第四步。

以上就是共享内存的原理。

我们学习os一定要具有跳出来的能力,首先我们要知道的是难道系统中就只有这两个进程要进行进程间通信吗?不要忘了在os中是存在成百上千的进程的,肯定是存在很多的进程要进行通信的,那么os在物理内存中就会开辟出很多的空间用于给这些进程进行进程间的通信,而既然通信的进程有很多,那么os开辟出的空间就有很多,那么os需不需要将这些进程管理起来呢?大(同时在os也是存在再同一时段存在多个进程都要进行通信的情况的,也就是os要开辟多个空间用于os支持同时存在多个共享内存,用于给与其它进程进行通信,也是存在多个共享内存)答案自然是需要的,如何管理:先描述再组织。由此一定是存在一个结构体用于描述共享内存的。这个结构体中一定存在一些属性块描述了这个共享内存多大,存在多少个进程指向这个共享内存(引用计数)。如果这个计数为2代表存在两个进程使用这个空间,如果为0代表这个共享内存可以被释放了。 然后我们使用一个数据结构来储存这些结构体对象,那么此时对共享内存的管理也就变成了对这个数据结构的增删改查。也就是我们的一个进程在释放共享内存的时候只需要让对应的引用计数渐渐即可。释放交由os来管理。

但是光有这些原理还不够我们先输出第一个结论:

第二个结论:

关于第二个结论我们首先要来提出一个问题,不要忘了我们的进程间通信肯定是两个进程之间进行的,那么现在我们的一个进程已经完成了映射,同时os也已经开辟好了空间,现在我们的另外一个进程只需要完成对应的映射即可,但是问题就是我们的另外一个进程怎么知道当前映射的这个地址空间就是我们通信时要使用的这个空间呢?因为在os中是存在多个共享内存的(第一个结论得到)

不要想着其中一个进程能够将这个地址直接交给另外一个进程,因为你都能将地址交给另外一个进程了,还有进行通信的必要吗?

那么os是如何保证的呢?这里我们只能使用文件简略的说明一下,待会写代码的时候我们才能够彻底的理解。

要解决这个问题首先就要求我们的每一个共享内存都要需要具有一个唯一性标识,以确保两个进程看到同一个共享内存(如同进程pid一样)。

这个具体是怎么做的呢?我们需要通过代码来认识一下。

那么既然需要写代码那么我们就暂且进入下一步:

快速认识系统接口

下面我们就来理解共享内存的系统接口。

使用共享内存的一个接口就是下面这个:

这个接口的作用就相当于第一步将我们的共享内存创建出来。

第一步完成之后,我们自然要将这个共享内存挂接到我们的进程上。

而这一步也是有一个单独的接口的:

调用这个接口的进程就会在自己的共享区(进程地址空间中)找到一个连续的空间,再使用页表映射到共享内存上,解决的就是第二个挂接的问题。

这个at就是英文单词attach的缩写。

如果你想要释放共享内存就需要使用最后一个接口了。

这些接口的参数和返回值我们待会在写代码的时候会说明。但是当一个进程调用了这个shmdt之后,这个接口做的事情只有一件那就是将让共享内存的引用计数进行了减减。但是这还是不够的,因为我们在物理内存中申请的那一片共享内存是需要我们手动去释放的。这里我们就需要使用最后一个接口了。

这个ctl也就是控制那个单词的缩写,所以我们就可以通过这个接口完成对于共享内存空间的释放,但是除了释放之外这个接口还有其它的功能(比如修改和查询共享内存的属性也可以通过这个接口完成)。

除此之外,在shmget这个接口中存在一个参数:

这个参数就和我们上面说的共享内存的唯一性标识有关。

学习代码编写

接口我们已经认识到了下面我们直接来写代码。我们下面要学习的就是使用共享内存进行进程间通信,而共享内存也是允许毫不相干的两个进程之间进行通信的。在未来的所有的通信方案中只有管道是需要父子/血缘关系的要求的。

首先我们将我们需要的三个基本文件准备好:

下面是Makefile文件:

下面我们首先来写server.cc文件:

首先就是在物理内存上为我们申请一片空间。

shmget这个函数的功能就是申请一个system V的共享内存。

需要的头文件如上图。上面的ipc就是进程间通信的英文缩写。

shmget函数

如何获取key

下面我们要使用这个接口自然要认识这个接口参数的作用。

首先中间的size肯定是你要申请的共享内存大小

这个函数的返回值就是如果你创建共享内存成功了,那么这里就会给我们返回一个标识符。

申请失败-1返回错误杯设置。如果申请共享内存成功了,那么这里的返回值我们就可以用来标识当前的某个共享内存的资源。这个整数(返回值)的作用你就可以认为类似于我们之前所认识的文件描述符的作用。但是这个返回值并没有很好的接入到文件系统,所以这里只是类似。因此共享内存是比较独立的一种,至少不能做重定向的功能。但是你之后要对这个共享内存的种种操作都要根据这个返回值来决定,关于shmflg之后说明,现在我们来看一下这个key,在shmget这个函数中存在一个参数key,那么这个key是干什么的呢?

这个key就是某一块共享内存的唯一性标识。回到上面两个进程要进行通讯的场景,现在我们的某一个进程调用系统调用创建了一块空间作为共享内存,然后os将一个数字(数字到底是多少不重要,但是这个数字在所有的共享内存块中是唯一的),然后在写代码的时候我们是能够让两个进程看到同一个数字的,此时的另外一个进程就可以去遍历所有的共享内存块,根据这个数字就能够让两个进程看到同一份共享内存(每一个共享内存块都有一个结构体对象描述,并且都保存在一个stl容器中)。总结就是通讯双方约定好一个数字一个进程创建好共享内存,另外一个进程去遍历寻找特定数字指定的哪一个共享内存。理论上来说你随意写一个数字即可,但是这种方法容易造成我们写的数字和共享内存中的某一个内存发生冲突,所以我们一般是建议使用一些算法计算出一些数字,这些数字就有较低的概率会和共享内存中的其它块的id冲突,切记是较低的概率,不是完全没有概率,如果发生冲突了换数字即可。那么这里就又要介绍一个接口了:

ftok这个接口就能够根据文件所在的路径和一个标识符形成一个特定的IPC key。虽然这也是一个调用,但是这个调用并不会在os中做任何事情,因为这是一个算法函数。未来你只要传递一个字符串和一个id就能够在底层通过自己的算法,形成一个数字。那么现在我们的两个需要通信的进程只要在使用这个ftok函数的时候传递两个相同的参数,那么形成的也一定是两个相同的key。这样也就让两个进程约定好了一个数字。这就为我们进程间通信的前提:让两个进程看到同一份资源做了基础。

那么这里我们就再创建一个comm.hpp

然后我们在server.cc中调用ftok函数

然后我们将上面写的这一段完全不变的复制粘贴到client.cc中这样client和server就能够得到相同的一个key了。

此时我们就能够回答上面的那两个问题了,如何保证两个进程能够识别到的是同一个共享内存,因为我们使用ftok生成出的key是一样的那么两个不同的进程就能够看到同一个共享内存。一个进程在创建共享内存的时候将这个key写到共享内存的结构体中,另外一个进程通过这个key就能够找到这个特定的共享内存。下面我们打印查看一下这个key值是否相同。

运行之后我们可以看到确实形成了两个相同的key。

然后我们将这些代码整合起来,将其放到comm.hpp中。让我们的代码的可读性增加。

然后我们的cilent.cc和server.cc中就只需要下面这样写即可。

其实这里的key_t也就是int。

下面我们就来使用shmget创建共享内存。其中第一个key_t我们已经知道了。第二个size是大小,那么第三个shmflg是什么呢?要回答这个问题,我们就需要再去思考另外一个问题,那就是现在我们已经知道了如何创建共享内存(shmget),但是并没有讲解如何获取共享内存啊,现在一个进程可以通过shmget去创建共享内存,但是另外一个进程要如何通过key去获取共享内存呢?那么原因就是shmget这个接口既能够创建共享内存也能够获取共享内存。如何做到的呢?就是通过shmflg这个参数。这个shmget接口具有以下几个不同的shmflag

shmflg的选项

选项如下:

其中的重点选项:

IPC_CREAT和IPC_EXCL中间使用下划线分割并且为全大写,说明这既是两个宏,这个宏其中只有一个比特位为,使用按位或,就能够让两个宏所对应的功能都起到效果。

这也说明了,shmget函数在底层也是通过key去判断当前共享内存是否存在的。

IPC_EXCL这个选项不单独使用单独使用没有任何的意义。

这个选项通常和IPC_CREAT按位或一起使用。

这个混合选项就是为了保证当前申请出来的一个共享内存是一个全新的唯一的共享内存。

下面我们就来使用一下这个接口。

在server.cc中:

现在我们创建好了一个共享内存。是否可行呢?

这样代表一个全新的共享内存已经存在了。至于shmid的具体数字是多少我们并不关心。因为在这里我们可以认为这是没有规律的。和文件描述符不一样。

那么现在我们的共享内存已经创建成功了,但是我们怎么知道已经创建成功了呢?

我们可以使用ipcs -m这样的指令去查看共享内存的信息。

可以看到当前我确实创建了一个共享内存。其中这个共享内存的大小也是我们写的4096

其中第一个key就是我们在上面写的那个共享内存的key(只不过这里是16禁止),shmid就是我们上面通过算法得到的shmid,然后owner代表这个共享内存是谁创建的,perms是共享内存的权限,而bytes自然是大小了,而nattch代表当前共有多少个进程挂接到我们的这个共享内存上。这里显示是0个代表没有进程挂接到这里。但是这里不要忘了此时我们server进程已经退出了。但是这个共享内存还是存在的。此时我们在运行server

证明这个选项确实是创建了一个全新的共享内存。

那么还有一个问题:我的server进程不是已经退出了吗?但是这个共享内存还是存在的,难道这个共享内存不会自己释放吗?这就和我们之前所说的文件不一样了。文件本身的生命周期是随进程的,进程退出,该进程打开的所有文件就会被自动的释放。但是共享内存是必须由用户通过指令或者是代码进行释放的,如果用户不进行释放那么当os退出的时候,也会进行释放,也就是共享内存的生命周期是随内核的。

这也证明了共享内存所在的模块和进程所在的模块是不一样的,进程退出了,但是不影响我们的共享内存(除非代码/指令释放/系统退出自动释放)。

那么如果现在我想要释放这个共享内存,要怎么做呢?

使用指令

ipcrm -m

那么为什么要使用shmid,为什么使用key不可行呢?

即我们要如何理解shmid和key呢?两者的区别又是什么呢?

其中的key:不要在应用层使用,只用来在内核中标识共享内存的唯一性

其中的shmid:我们在代码中使用这个共享内存的时候,我们使用shmid来进行操作共享内存。

总结就是:key用于系统内核来标识当前的共享内存,而shmid方便我们程序员进行编程。

这个key就和我们之前学习的文件描述符fd类似,而shmid就和FILE*比较类似。当然我们的key不能在应用层使用,但是fd我们在写代码的时候也是可以使用的。

这就是为什么我们在删除共享内存的时候要使用shmid,因为key只做唯一性标识,而我们使用的指令说白了也是程序也是ying'y,那么我们也需要使用shmid来进行删除。

下麦呢我们再来理解共享内存的权限。关于这个权限更加详细的知识在之后我会讲解,但是这里我们需要知道虽然共享内存和文件没有关系,但是共享内存还是存在权限的。因为是存在某些情境下,我们只想让某些进程对于共享内存是只写的,有的是只读的。有的是可写可读的。那么如何增加权限呢非常简单:

此时创建出来的共享内存的权限就是666了。

然后因为我们创建共享内存大小的时候写的共享内存的大小是4096,所以这里显示的是4096,但是如果这里我们写的是4097呢?

可以看到这里确实变成了4097。虽然符合预期,但是我们这里建议共享内存的大小设置为4096的整数倍,那么为什么要设置成为4096的整数倍呢?原因就是os在开辟共享内存的时候是以4096作为基本单位开辟的,也就是虽然你写的是4097但是os给你的空间却是4096*2,但是你不可以使用除了4097大小范围内的空间,使用超出范围的空间会直接报错。即os会给你开辟这么多的空间,但是你无法使用多余的空间,由此就会造成内存的浪费。所以这里最好是使用4096的整数倍去开辟共享内存的大小。

到这里第一个结构也就解决了

shmat函数

其中的第一个参数自然就是上面shmget函数的返回值了。即当前我要将哪一个进程挂接到id为shmid的共享内存上。那么这里的第二个参数shmaddr又是什么呢?首先这里我们直接设置为nullptr就可以了,这里的意思就是你想要手动的将共享内存挂接到你指定的地址空间中某一个连续的空间上。因为我们对于地址空间的使用情况又不了解所以这里就不使用手动了,使用nullptr让os帮助我们去选择。

最后一个shmflg就是当前的进程说是以读的方式挂接还是以写的方式挂接,这里我们不管因为我们的共享内存有权限去进行控制。

所以我们想要将一个进程挂接到某个共享内存上需要的函数参数就已经决定了,但是难点在于返回值,这个函数的返回值又是什么呢?

返回值也就是如果挂接失败了返回-1,如果挂接成功了就会返回这个共享内存挂接在进程地址空间中的那片空间的起始地址(虚拟地址)。之后我们就可以通过这个虚拟地址去访问这个共享内存了。

也就是挂接成功之后,我们的某个进程凭空的多出来了一块空间。类似于c语言中的malloc。

为了让我们知道确实挂接到了共享内存中我增加了两个sleep()(在挂接前和挂接后)。

可以看到挂接数确实增加到了1,当进程退出之后挂接数又重新变成了0.所以这里的nattch代表的就是当前共有多少个进程关联到当前的共享内存中。当进程退出之后,共享内存中的引用计数减减,这里的引用计数减减的原因自然是进程退出了,对应进程的页表自然也会被清理,所以也就让对应的共享内存中的引用计数减减了,自然也就让nattch变成0了。拥有了引用计数,我们的共享内存也就能够知道当前存在多少个进程挂接到当前的共享内存了。

当然使用shmat挂接也是可能会失败的,失败我们这里就不做考略了。

shamdt函数

那么如果我们想让进程在运行期间能够自动的完成对共享内存的去关联操作是否可行呢?答案自然也是可行的。

这个函数的参数就是当初进程在挂接时获取的起始地址。

如何理解呢?我们要解除挂接也就是要完成对进程页表的修改,现在我们知道了起始地址,然后还知道对应的共享内存的大小,我们自然能够在虚拟地址空间中一个一个的往下释放,最后让共享内存中的引用计数减减,并且完成对页表的修改就完成了解除挂接。

所以我们之后可以看到的现象就是挂接数在进程运行期间由0变成1再变成0.

符合现象。

shmctl函数

最后我们来学习一下共享内存的释放函数。

这个函数的作用存在很多,其它的作用我们在之后会说明(修改,删除,查询没有增加),这里我们就暂时只说明释放共享内存的作用。

其中的shmid代表你要控制的是哪一个共享内存,而cmd则是一个选项代表着你要进行删除的控制。

其中的IPC_RMID就是完成删除的选项,其它的选项我们之后会介绍。最后shmctl的最后一个参数是一个结构体。

这个结构体中包含了共享内存的属性信息。也就是这个结构体可以填充共享内存的很多属性信息。这个结构体起始就是内核用来描述共享内存的那个结构体的子集。这个结构体提供给用户,让用户能够通过这个结构体完成对共享内存某些属性信息的修改和获取共享内存的部分信息。这也是ipcs -m命名底层的实现原理。这个参数我们暂时不用可以设置为0。

最后我们再来看一下这个函数的返回值。

删除成功0返回,删除失败-1,并且错误被设置。

因为最后的运行截图,太多了,这里就不再截图出来了。但是运行结果是没有问题的。到这里我们就完成了整个共享内存的生命周期的代码级别的管理。

最后为了让我们的代码的可读性更强我们将对应的接口做一下简单的封装。

最后我们使用共享内存的目的还是为了让进程之间能够完成通信。现在我们只是完成了一个进程的挂接。

需要让client也要完成对于同一个共享内存的挂接。

现在我们来处理client端的代码。

对于client端来说,共享内存已经存在了,我们的cilent端已经不需要再去创建一个新的共享内存了,只需要获取到同一个shmid之后,将共享内存挂接到自己的地址空间中即可。使用shmat即可完成对应的挂接。

这里我将这个共享内存当作了一个字符串去使用,所以这里client完成挂接之后,我将这个共享内存的返回值地址由void修改为了char,当然你也可以转为int*去使用。同时这里的server能够创建一个共享内存那么就能够创建多个共享内存。只要在调用shmget接口的时候,传递过去的key是不同的即可。

最后让我们来完善client端的代码。

server端:

最后让我们来测试一下,这里我是先让server端创建共享内存,在server端创建完成之后,server端会休眠,这个时候让client端运行起来,让client链接到共享内存,server端在休眠完成之后也会挂接到共享内存上,最后client解除挂接,然后server端解除挂接,之后server端会将共享内存进行销毁。

监视截图:

可以看到链接数确实是从0变到1(client挂接),再变到2(server挂接),然后再变成1(client解除),然后变成0(server解除),最后销毁(server销毁)。

当两个进程都挂接到共享内存的时候,我们就完成了进程间通信的大前提:让两个进程看到同一份资源。但是到现在为止我们还没有让两个进程进行通信。所以无论是共享内存还是管道都是为了准备让两个进程看到同一份资源。真正的通信只有准备好之后才能进行通信。为什么这么复杂呢?因为进程在建立的时候都在强调每一个进程都是具有独立性的(每一个进程都具有自己的pcb,页表等等,每个进程的资源都是互相独立的)。为了保护这种独立性又要让两个进程之间能够进行通信,由此才要做上面那一大堆准备的工作。

最后总结一下:共享内存是通过两个进程使用相同的key值(再深入一点就是ftok函数的两个参数一样)来达到让两个进程看到同一份资源的。

但是这里我们思考一个问题:为什么这个必须由我们用户去指定生成,然后在创建/挂接共享内存的时候由我们用户将这个key设置到内核中呢?

为什么os不能自己在底层形成一个随机数然后让os去维护呢?然后由用户去获取shmid和key呢?总结就是为什么这个key要由我们用户去生成指定。答案很简单,对于server端而言,创建完成之后拿到这个共享内存的shmid很容易,但是对于client呢?因为没有key,client怎么知道那么多的共享内存中哪一个是用户需要的那一个共享内存呢?所以我们在用户层形成key不就是为了让通信双方都能看到这一个key吗?这才是为什么我们要在用户层形成key的原因。

进行通信

下面我们就来让server和client端进行通信。

这里我们让server端直接进行读取。client端进行写入。

client:

然后让我们的server端每相隔一秒就打印一次s中的内容,我们查看一下共享内存是否和管道一样具有同步的机制

server端:

运行现象:

可以看到的是虽然server端是每隔5秒才会输入内容,但是client端是不管这些的,server端一直在读取信息,也就是共享内存和管道不一样,共享内存是没有同步机制的。各自跑各自的。也就可能会造成下面的这种情况,我们的一个读端进程正要读取共享内存中的一句话hello world,但是读端才刚把hello读完了,world还没有读取,就已经被写端覆盖了。总结就是我在写的时候你来读,而我在读的时候你又来写,这种情况我们称之为多执行流并发访问公共资源时的数据不一致的问题。但是我们在使用共享内存时也是需要同步和保护的机制的。(把共享内存当作是一个公共的资源时是一定需要同步和保护的机制的)一般是利用system V版本的信号量来保证的。但是这里就不提及了。那么不使用信号量能否做到同步呢?当然可行,那就是使用我们之前学习的管道来帮助共享内存实现同步。

利用管道实现共享内存级别的同步

首先这里我们需要做到的情景是我们的写端写一个字符,那么我们的读端就读取一个字符,不要出现上面的情况。

首先我们就要建立一个简单的管道:

其中的FIFO_NAME和MODE都是宏。

然后我们来写我们的server端的代码,我们的server端先建立好我们的管道,然后将自己挂接到共享内存上面,然后我们以读的方式打开我们的管道,后面的逻辑一样(接触挂接和释放共享内存)。这里不要忘了我们的管道是自带有同步的机制的,所以当我们的server端打开管道的时候,如果另外一个端口不打开管道server端就会被一直阻塞在这里。

下面是我们的client端要做的事情,client端要做的事情很简单,任然是先将自己挂接到共享内存上,然后client端以写的方式打开管道。还有一些逻辑请看代码:

server端:

client端:

下面是我们的运行截图:

可以看到借助管道我们确实实现了共享内存级别的同步。从这里我们要知道在正常并且是多执行流的情况下,共享内存是需要我们去完善同步和互斥的机制的。上面我只是实现了同步的机制,至于互斥的机制,因为我暂时我还没有学习到那里,所以就不实现了。

最后我们需要注意共享内存的通信方式,不会提供同步机制,共享内存是直接裸露给所有的使用者的,一定要注意共享内存的使用安全。还有一点便是,当我们的一个进程将abcde这样的信息写到共享内存之后,读取端在读取的时候只是将共享内存中的数据拷贝了出去,如果你想要删除共享内存中的数据你必须自己动手。

如何做呢?

假设上图就是一个共享内存,其中我们在最开始的八个字节处写了对应的信息(管理共享内存的信息),这里就是假设第一个信息代表读端要从哪里开始读取,而当现在已经读取了A之后了,让rpos指向的位置往后移动一个此时我们就可以认为将A清理了。这也就是说,我们要将一个信息从缓冲区中丢弃,并不一定要删除这个数据,让这个数据无效也是一种删除数据的方法。

这种方法就和我们曾经使用过的free是很相似的。因为我们在void* m = malloc(100),但是在释放的时候只是free m,你在free的时候并没有说明你malloc了多大的空间,但是依旧能够使用free直接释放空间,因为其实os在帮助我们申请空间的时候,申请的有可能是大于100大小的空间,而那些多余的空间就是用来储存这片malloc出来的空间的信息的。和我们上面所说的原理是很相似的。

最后我们再来修改一下我们的这个server端的代码,这一次我将server端中创建共享内存,创建管道,以及挂接的代码都放到Init类的默认构造函数中。然后我们再给Init这个类增加一些成员。代码如下:

这样我们在server端就只需要创建一个Init对象即可。

server端:

至于client端则不变。

运行结果:

最后我们来总结一下共享内存的使用特点:

那么共享内存为什么是最快的呢?

原因如下:

达能我们使用管道进行通信的时候,os会在内核中新建立一片空间A进程将信息拷贝到管道中然后B进程再从管道中获取这个信息。而共享内存则是将这个缓冲区直接写到了两个进程中,A进程写了信息B进程马上就能得到,减少了拷贝信息的次数。由此共享内存的速度是非常快的。并且上面的这张图还没有考虑到硬件,如果考虑到硬件,那么管道需要拷贝的次数只会更多,而对于共享内存而言,我们可以做到让A进程直接让输入的信息拷贝到共享内存中,那么B进程就直接得到了这个信息,B再从共享内存中将这个信息拷贝出来,只是两次拷贝就完成了加上硬件的数据迁移。而如果是管道需要几次拷贝呢?

四次拷贝(硬件到进程再到管道再从管道到另外一个进程最后到另外一个硬件),并且我没有加上语言自带的缓冲区。

由此可以说共享内存的速度是最快的。

从这里我们也要得到一个信息那就是:凡是数据迁移,都是拷贝。

由此我们也就能知道了我们之前所学习到的write和read接口所谓的往管道中写入信息,和往管道中读取信息,本质都是将管道的信息拷贝到进程的缓冲区中/将进程缓冲区中的内容拷贝到管道中。

shmctl接口的其它作用

在我们上面的代码中,我们已经知道了shmctl的第一个作用就是能够删除共享内存,下面我们再来看一下shmctl是如何和获取共享内存的属性信息的。

首先我们了解一下shmctl的三个选项的作用:

最后有一个选项的作用我们在上面已经了解过了,而修改信息的选项在这里我们暂时不做展示,我们来使用第一个选项获取一下共享内存的信息。

首先我们来看一下储存共享内存的信息的结构体的组成:

可以看到在这个结构体中除了共享内存的一些基本信息之外还嵌套了一个结构体(ipc_perm)

ipc_perm的内容如下:

可以看到在这个结构体中就包含了一个key(这个key正是我们当初创建共享内存时使用的key)。而我们也就是通过上面的结构体去获取共享内存的信息,这里我能够获取是因为这个信息在内核中之前就已经建立好了。

下面我们就来获取一下这些信息

例如这里的struct shmid_ds就是一个内核结构体这里我们也是可以直接进行创建的。

最后让我们来打印看一下这些信息

可以看到信息都是能够对应上的。

那么我们能够获取这些属性信息意味着什么呢?

这就意味着在os中一直是在维护着这些属性信息的。也就是我们在之前所说的

共享内存 = 共享内存的空间+共享内存属性

到这里我们的共享内存就结束了。

希望这篇博文能对您有所帮助,如果发现了任何的错误,欢迎指出,如果发现了任何的错误,欢迎指出。

相关推荐
Victor3569 小时前
MongoDB(2)MongoDB与传统关系型数据库的主要区别是什么?
后端
JaguarJack9 小时前
PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南
后端·php·服务端
BingoGo9 小时前
PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南
后端
Victor3569 小时前
MongoDB(3)什么是文档(Document)?
后端
牛奔11 小时前
Go 如何避免频繁抢占?
开发语言·后端·golang
想用offer打牌16 小时前
MCP (Model Context Protocol) 技术理解 - 第二篇
后端·aigc·mcp
KYGALYX17 小时前
服务异步通信
开发语言·后端·微服务·ruby
掘了17 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
爬山算法18 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate
Moment18 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端