本篇文章,继续来和大家分享与Linux相关的知识。本次内容主要会涉及到共享内存。
直接原理
我们知道,进程间通信的本质是,先让不同的进程看到同一份资源。
假设现在有A和B两个进程,操作系统在物理内存,申请了一段空间。然后,把这段空间的通过页表,映射到A进程的共享区。映射到共享区后,会有占用一段空间,把这段空间的虚拟地址的起始地址,返回给进程A。进程A就可以看到这段空间了。我们用同样的方式,映射到进程B的共享区。这样A和B进程就能看到同一份资源了。这个就是共享内存的原理。
建立共享内存,我们可以理解成以下三步:
第一步:申请内存
第二步:挂接到进程地址空间
第三步:返回虚拟地址的起始地址给进程
那如果我们要释放共享内存呢?
第一步,解除进程和共享内存的关联
第二步,释放共享内存
在建立和释放共享内存的过程中,进程是直接做的吗?不是,进程作为需求方,告诉执行方操作系统,自己的需求。执行方根据需求方的需求进行操作
申请共享内存的需求只有一个吗?在操作系统中,会存在很多的共享内存。这么多的共享内存,操作系统需不需要管理起来?怎么管理?先组织,后描述。操作系统中,会存在一个内核数据 结构体,用来描述共享内存。
直接代码
理解完原理,写代码就很简单了。
建立共享内存,我们用到的函数叫shmget。
shmget函数的第二个参数是要建立共享内存的大小,第三个参数是选项。我们会用到IPC_CREATE和IPC_EXCL两个选项,IPC_CREATE的作用是如果你申请的共享内存,存在就返回获取,不存在就创建。IPC_CREATE和IPC_EXCL组合使用的作用是,存在就出错返回,不存在就创建。IPC_EXCL不单独使用。它的返回值,是共享内存的标识符,这个有点类似文件标识符。
那我们怎么保证不同的进程看到同一个共享内存呢?这就不得不谈谈第一个参数key了
对于key,我们需要有5点认识。
1.key是一个数字,它能让不同进程标识同一块共享内存
2.第一个进程通过key创建共享内存,第二个往后的进程,只要拿着同一个key就可以和第一个进程看到同一个共享内存了
3.对于一个已经创建好的共享内存,key在哪里?key在共享内存的描述对象中。
4.第一次创建共享内存的时候,必须有一个key
5.key,类似路径,具有唯一性
我们一直在说key,key怎么来?通过函数ftok获取。ftok函数的第一个参数是路径名,第二个参数是一个整数,随便设。
ftok是一种算法,在获取key的时候,并不会查这个key有没有被使用。这也就意味shmget建立共享内存会出错。shmget创建共享内存失败,主要有两种原因:
第一种:共享内存不足
第二种:构建的key唯一性不足
创建共享内存的方式,大致了解了,我们来实践一下
common.hpp
processa.cc
编译运行,processa程序,就可以看到共享内存的shmid和key了。我们可以通过ipc -m指令来查看,确实存在一个和程序打印的shmid和key同样的共享内存。
ipcs -m
在processa的程序退出之后,共享内存也并没有释放。这说明什么?说明了共享内存的生命周期是随内核的!用户不主动关闭,共享内存会一直存在。除非内核重启或用户释放
shmid和key都能标识共享内存,那它们的区别是什么呢?key在操作系统内标定唯一性。shmid只在进程内,用来表示资源的唯一性!
那如果我们使用ipcrm -m指令释放共享内存,我们用key还是shmid呢?
通过实践,答案很明显是shmid。用户在管理共享内存的时候,用的是shmid。
我们来简单了解ipcs -m显示的属性是什么意思。
key和shmid就不用说了。owner就是谁创建的它,perms是权限,也就是文件的权限。之前说Linux下,一切皆文件,但共享内存这里搞了个特殊。bytes是共享内存的大小。nattach是关联数,也就是有几个进程和这个共享内存相关联。
如果说我们想给共享内存,设置权限,只需要在shmget函数的第三个参数中,加上权限即可。
编译运行,然后通过指令查看就可以看到共享内存有了权限
共享内存只需要由一方来创建,另一方直接使用就可以了。我们把接口做一下修改,顺便把key改成以16进制的方式打印
共享内存能创建了,下一步,就是将共享内存挂接到进程的共享区,让进程看到。
怎么挂接呢?用shmat函数。
shmat的第二个参数,告诉系统把共享内存挂到共享区的哪一个位置,我们直接传nullptr,让系统自己来决定就可以了。第三个参数,传零就可以了。返回值是系统实际挂接共享内存的虚拟地址的起始地址。
shmat下面的函数shmdt函数,是用来解除进程和共享内存的关联的
它的用法很简单,只需要把shmat的返回值,传给它就可以了
那我们如何直接释放物理内存呢?用函数shmctl
shmctl的第二个参数是选项,告诉系统你是要释放共享内存,还是要修改共享内存的大小等等。第三个参数是获取共享内存的属性,拷贝出来进行查看
方法了解清楚了,我把代码补充一下
processa.cc
编译运行,并打开监控脚本。我们就能看到共享内存属性nattach的变化,由零变1,再由1变成0
下面,我们把通信部分加上,并且完善processb.cc
processa.cc
processb.cc
编译运行,processa和processb就可以正常通信了
processb.cc源文件中,我们通信的方式,可以更加简便一些。一旦有共享内存挂接到自己的地址空间中,你直接把它当成自己的空间使用即可
processb.cc
编译运行,我们也能正常的进行通信
如果我们想查看,共享内存,只需如下图一样操作即可
processa.cc
编译运行,就能看到属性被打印出来了
共享内存的特性
我们总结一下,共享内存的特性。可以简单的理解为三点:
第一点:共享内存没有同步互斥之类的保护机制
第二点:共享内存是所有的进程间通信中,速度最快的。原因在于,共享内存的拷贝次数比较少
第三点:共享内存内部的数据,由用户自己维护
共享内存,没有互斥保护机制。可我们希望它具有互斥机制。如果客户端没有输入,服务器就阻塞等到我们的信息,而不是一直向显示器打印
那我们怎么加上互斥机制呢?我们还没学过互斥锁,但是我们学过命名管道呀!命名管道具有互斥机制,我们可以用命名管道,来通知对方,传输信息使用共享内存。
理解了思路,实现起来就很简单了
上一篇文章,我们已经写过创建销毁命名管道的代码了,直接复制到common.hpp中即可
common.hpp
processa.cc
processb.cc
编译运行,就可以能实现同步互斥的通信
好了,到这里,我们本次的分享就到此结束了,不知道我有没有说明白,给予你一点点收获。关于更多和Linux相关的知识,会在后面的文章更新。如果你有所收获,别忘了给我点个赞,这是对我最好的回馈,当然你也可以在评论发表一下你的收获和心得,亦或者指出我的不足之处。如果喜欢我的分享,别忘了给我点关注噢。