共享内存是Unix/Linux编程的一个特别领域——不是说Windows上没有类似的机制,或者说这个东西比较高深——不是的,只是说这个东西一般情况下不使用。
目录
一、什么是共享内存
共享内存其本质,就是一块能在多个进程之间共享的内存。在早期的实地址模式下,所有进程都直接使用所有物理内存,无所谓共享不共享,在虚拟地址时代,每个进程有独立的地空间,互不干扰,这样共享内存才有了存在的基础。
进程的虚拟地址空间终究要映射到真实的物理地址上,这是由操作系统控制地址映射表来实现的(同时也必须有CPU支持)。操作系统如果让两个进程的一部分虚拟地址映射到同样一块物理内存,那么这两个进程对这块物理内存的修改当然会同时反映在两个进程内,这就是共享内存。
很容易理解,共享内存是跨进程交互的最快方式——这简直不需要论证。
不过大部分情况我们并不需要共享内存带来的速度优势,毕竟,共享内存的好处也意味着坏处:多程序操作数据容易损坏、没有任何并发保护、可以在程序之外删除。其实吧,共享内存出来的时候还没有线程呢,多线程具有共享内存的所有好处——除了共享内存是脱离程序存在的。促使我们选择共享内存的根本原因,就是这一点。
共享内存脱离进程这一点,在特定应用场景下很有用:
- 大数据量又要求高速处理(数据库操作速度完全不可接受)
- 数据加载逻辑复杂且耗时(按小时计)
- 需要快速的磁盘备份和恢复(按分钟计)
- 数据使用者众多,来自不同的业务,但以只读访问为主
这样的场景非常适合共享内存:
- 全内存数据,速度无可挑剔
- 数据加载和使用分离
- 可以编写独立的备份恢复程序,由管理员负责
- 数据脱离独立于程序存在,程序停了数据还在
- 可在不同业务之间共享数据
二、使用共享内存的注意事项
2.1 互斥
使用共享内存一定要牢记:共享内存是没有任何保护的。所以如何合理互斥就成为关键(这当然是所有并发操作的关键)。
由于共享内存是跨进程的,所以POSIX线程库的互斥功能是不适用的,必须使用系统级互斥。我以前一直使用信号量,后来改用C++11的原子库,具体可以参看我的其他文章。
2.2 读写分离
我说的不是“一块数据读、一块数据写”那种意义的读写分离。而是程序规划上的读写分离。
使用了互斥不代表程序就能正确工作,特别是程序来自不同业务,因此可能由不同人来编写,直接暴露数据给其他人绝对是个馊主意。
一定要写个库给别人用,确保库是正确安全的。
业务上最好写操作控制在自己手里,给别人的库压根就没有写功能。这就是为什么前面强调了数据加载的独立性。一个进程写其它进程只能读,这样容易控制。
2.3 管理和维护
由于共享内存是独立于进程的资源,必然需要额外的管理和维护。
首先要解决的问题就是共享内存的key或者ID如何存储。key是创建时指定的数值,在全系统必须唯一,每个key都会映射到实际的ID。实际访问共享内存用的是ID,存在额外的key的目的是提供类似TCP/UDP端口那样的“众所周知”的入口。
使用固定的key好处是不需要存储,只需要在文档里说明即可。坏处是如果冲突就无法解决,因为共享内存是单机范围内的,并没有“众所周知”的部分。
不使用固定的key(创建时使用IPC_PRIVATE做参数)则必须保存ID,根据项目实际情况使用合适的方式即可,比如写入配置文件、存储到数据库等等。
使用过程中需要辅助工具是不可避免的。比如:
- 如果ID不慎丢失,能否从所有共享内存中发现
- 重新创建是是否要先删除旧的
- 如果发生死锁,如何手动解锁
- 有程序异常退出后检查数据结构是否正确
- 任何时候检查数据结构是否正确
- 备份到磁盘、从磁盘加载
2.4 最大难点:地址不固定
共享内存连接到每个进程的地址是不确定的。虽然确实可以指定连接地址,但是限制很多:
- 必须确保地址空间还没有被使用,要求所有程序在所有情况都满足这个要求很困难,除非只有自己写的程序(某个大型数据库就是这样用的)
- 无法编写能自动增长的数据结构,动态申请的新共享内存更没法保证固定的地址
所以要设计自己的数据结构,不使用指针的。
三、基本编程
共享内存与普通内存的关键区别是三点:
- 必须先连接,连接之后得到一个指针指向一个内存块,然后就跟自己new的内存没有区别了
- 指针指向的内存是共享的,任何改动其它进程都能看到
- 连接地址是不确定的,意味着你不能在里面放指针(允许使用固定地址,但是只有特殊场景可以这样做)
编程细节见这里:
(这里是结束)