啥?进程通信的共享内存都不知道!
--------------当日上午,大白正在找借口请假面试-----------
一个阳光明媚的中午,大白在领导办公室
👨💻 大白:领导我这牙疼还是没好,下午还得去找那个医生开点药,不行不行,太疼了....我现在就得去了。
👨💼 领导:......
--------------------当日下午,腾讯大楼--------------------
👨🏫 面试官:大白,上一轮的面试官反映你水平不错,我看你和上一轮的面试官聊了 进程通信中的管道 ,那么我们今天要不接着上个话题聊一聊吧。你还用过其它方式进行进程通信吗?
👨💻 大白:除了上次讲的管道的方式外,我还经常用 共享内存 的方式进行进程通信。
👨🏫 面试官:那我们今天就好好聊聊共享内存这种进程通信方式吧。
----------------------面试正式开始------------------------
👨🏫 面试官:要不你先简单说说什么是 共享内存 吧!
👨💻 大白:我们知道各进程之间是独立存在,互不影响的。有没有一种方式让这些进程之间产生联系呢?当然有!那就是共享内存。共享内存是进程间通信中最简单的方式之一。站在进程的角度来说,共享内存就是可以同时被多个进程访问的内存。由于所有进程共享同一块内存,因此这种通信方式效率非常高。
👨🏫 面试官:要不你再给我讲讲,为什么进程间的内存不是共享的吧?
👨💻 大白:(内心:啥?准备了好几天的进程通信,你问出来个这?)我想想啊......其实这个问题也还比较容易想明白了。举一个例子,假设有 2 个进程同时想让某一物理地址保存一个值,A 进程想让这个物理地址保存 1,B 进程想让这个物理地址保存 2。那么这个物理地址到底应该保存哪个值?所以,为了将每个进程隔离开,设计者就想到一个办法,操作系统会给每个进程分配一个虚拟地址。然后将不同进程的虚拟地址和不同内存的物理地址进行映射。每次进程想要写入数据,先访问虚拟地址,然后内存再将这个地址转换成物理地址,这样不同进程运行的时候,写入的是不同物理地址,就不会有冲突了。这就是进程独享内存空间的原理。我下面给您画个图。实际中虚拟内存和物理内存都会被分成大小相等的页,然后进行映射。但是由于我们这次面试的重点不在此,图就简略一点,表明关系就好。
👨🏫 面试官:你刚才提到了 虚拟地址 ?为什么要引入虚拟地址呢?运行过程中还得进行虚拟地址和物理地址进行转换。我看看物理内存有多大,直接把一段物理空间交给一个进程不好吗?然后这段空间不允许别的进程进行操作。这样不更省事?
👨💻 大白:(内心:这个面试官怕不是个哈皮吧...)嗯...是这样的,原因主要有 3 个方面:
- 操作系统是不希望一个普通的进程可以直接对物理地址写数据的。如果一个普通的进程可以随意的向物理地址中写数据。那么一个恶意进程一旦知道别的进程的物理地址,那不是很容易就把别的进程的数据篡改了嘛。
- 每个进程在创建之初,它所需要的内存大小都是不确定的。如果按照您的说法直接给进程分配固定的物理内存,假如两个进程在创建之初都直接各自分配了 1G 的物理空间。但实际运行起来,A 进程只用了 100M,而 B 进程需要 1.9 G。那么给 A 进程分配的空间就浪费了,而给 B 进程分配的空间又不够。都采用虚拟地址,表面看上去每个进程都可以独占内存的所有空间。在进程运行的途中再对虚拟地址和物理地址进行转换,可以有效的利用空间。甚至在内存不足的情况下,还可以把进程的内存存到硬盘里,切换到该进程时再从硬盘读取。
- 虚拟内存可以为每个进程提供一个一致的地址空间,这样程序员就不需要管理内存了,这也降低了编程的复杂度。
👨🏫 面试官:可以可以,没难住你。那你现在讲讲进程通信为什么又要共享内存了吧?
👨💻 大白:因为有时候两个进程需要进行大量的通信,并且传递的都是比较大的数据。那么采用管道或者消息队列的方式就不方便了。这不如两个进程都拿出一块虚拟地址,映射到相同的物理内存中。这样进程间需要传送的数据就不需要来回拷贝了,这边一写那边立马看到了。共享内存理论上是最快的进程通信方式,不过有个弊端就是不能跨物理机进程通信,如果需要跨物理机进行进程通信,建议用套接字。
👨🏫 面试官:共享内存让进程间的通信更加简单,效率也不错。但是,这种方式也存在一个比较明显的缺点—没有提供同步的机制。你简单说说该如何解决吧?
👨💻 大白:嗯嗯,确实!我们需要通过一些手段保证在数据被写入之前不允许其他进程从共享内存中读取。比较常见的解决办是通过 信号量 来进行同步。
👨🏫 面试官:我之前就听说你八股文背的贼溜,现在看来果然名不虚传。我想看看你代码能力,你给我用代码实现一下共享内存可以不?
👨💻 大白:没问题呀!首先我给您讲下思路吧!分四步就可以完成啦;
(1)既然需要用共享内存,首先需要创建一个共享内存或者得到一个共享内存。这一步要用到一个函数就是 shmget
。
int shmget(key_t key,size_t size, int flag);
//key:用来定位共享内存
//size:用来指定共享内存的大小
//flag:用来表示创建共享内存的方式,如果赋值是 IPC_CREAT 表示创建一个新的
//返回值:共享内存标识符
(2)通过第一步创建好了共享内存,但是如果一个进程想要访问这段共享内存,那么就需要将共享内存加载到自己的虚拟地址空间中。而加载的这个过程就需要用到下面这个函数。
void *shmat(int shmid, const void *shmaddr, int shmflag);
//shmid:传入共享内存标识符
//shmaddr:指定共享内存映射的地址
//shmflag:标识内存关联后的读写权限
//返回值:返回共享内存映射到进程空间的起始地址。
(3)经过前两步,所有与共享内存进行关联的进程,就可以进行通信了。这一步不需要什么特殊的函数,直接往共享内存中写入,或者从中读取就可以啦。
(4)如果内存共享使用完毕,那么就需要解除绑点,然后再删除共享内存对象。这需要用到下面两个函数。
int shmdt(void *addr);
//addr:共享内存的起始地址
void *shmctl(int shm_id, int cmd, struct shmid_ds *buf);
//shm_id:共享内存标识符
//cmd:对共享内存的操作,如果用IPC_RMID表示要将共享内存删去。
//buf:共享内存管理结构体。
👨🏫 面试官:好的,你的思路我明白啦,可以开始写代码啦。那个我怕你头文件的引用记不清,我直接给你写在下边吧。你一会直接引用就好啦。
//shared_memory.h
#include
#include
#include
#include //刚才介绍的几个函数都在这个库中
#include
#define PATHNAME "/home/dabai/server.c" //路径名,用它来获取共享内存标识符的key
#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096 //共享内存的大小
👨💻 大白:感谢感谢,那我就直接写代码啦。
//server.c
#include "shared_memory.h"
int main()
{
key_t key = ftok(PATHNAME, PROJ_ID); //建立共享内存需要一个区域标识符来标识共享内存区域,ftok把已经存在的路径名和整数标识符转换成一个整数 IPC 键值。
//如果key创建失败则返回值小于0,应该有个打印错误并结束程序的操作,为了代码简洁我就不写啦。
int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); //创建新的共享内存,返回共享内存标识符
//如果共享内存创建失败则返回值小于0,应该有个打印错误并结束程序的操作,为了代码简洁我就不写啦。
printf("key: %x\n", key);
printf("shm: %d\n", shm);
char* mem = shmat(shm, NULL, 0); //关联共享内存
//这里还是应该检查下是否关联成功为了代码简洁我就省略了
int i = 0;
while (1){
mem[i] = 'a'; //进程可以根据自己的需要在这里对共享内存进行写入或读出。
i++;
}
shmdt(mem); //共享内存去关联
shmctl(shm, IPC_RMID, NULL); //释放共享内存
return 0;
}
//这部分代码参考了 https://blog.csdn.net/chenlong_cxy/article/details/121184624,这篇博客的代码写的比较完善,大家如果感兴趣可以去学习下。
👨🏫 面试官:我记得你刚才跟我说更推荐用套接字?
👨💻 大白:我没说...是个幻觉。
👨🏫 面试官:对了,信号量也没细问。今天先放过你吧,我也该下班了,我给你通过面试了。不知道 leader 会给你加面不。套接字的问题等你入职后咱们聊一聊。
👨💻 大白:好嘞,感谢感谢。
参考资料:
- 极客时间《趣谈 Linux 操作系统》
- https://blog.csdn.net/chenlong_cxy/article/details/121184624
- https://juejin.cn/post/6844903507594575886
- https://snailclimb.gitee.io/javaguide/#/?id=%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F
推荐阅读: