先来了解下什么是共享内存,以及为什么需要共享内存。
共享内存就是使得多个进程可以访问同一块内存空间。不同进程之间共享的内存通常为同一段物理内存。共享内存是两个正在运行的进程之间共享和传递数据的一种非常有效的方式。进程可以将同一段物理内存连接到他们自己的地址空间中,所有的进程都可以访问共享内存中的地址。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。进程间通信的方式有很多种,比如消息队列、通道等,但共享内存无疑是最快的,因为它是直接对内存进行存取操作。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝;而共享内存则只拷贝两次数据:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。共享内存属于匿名页,匿名页是没有文件背景的,这样当进行内存交换的时候,是与swap分区交换。
注1:共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取,所以我们通常需要用其他的机制来同步对共享内存的访问,例如信号量。
注2:共享存储的数据格式,当成功导入一块共享内存后,它只是相当于一个字符串指针来指向一块内存。这也就给系统开发人员提供了发挥的空间,在内存使用范围内可以存储任意格式的数据,对于复杂的数据类型格式的解析、转化也带来了开发难度。
共享内存的通信原理
在Linux中,每个进程都有属于自己的进程控制块(PCB)和地址空间(Addr Space),并且都有一个与之对应的页表,负责将进程的虚拟地址与物理地址进行映射,通过内存管理单元(MMU)进行管理。两个不同的虚拟地址通过页表映射到物理空间的同一区域,它们所指向的这块区域即共享内存。
对于一个共享内存,实现采用的是引用计数的原理,当进程脱离共享存储区后,计数器减一,挂架成功时,计数器加一,只有当计数器变为零时,才能被删除。当进程终止时,它所附加的共享存储区都会自动脱离。
共享内存的生命周期
共享内存存在于操作系统用户空间,它不归属于任何进程。共享内存的生命周期与用户进程的内存生命周期是独立的。也就是用户进程销毁后,共享内存可能还在。
共享内存方式
1.基于传统SYS V的共享内存
通过此种方式申请的共享内存,可以通过ipcs命令查询,也可以运行ipcrm -m shmid(共享内存标识符)命令进行手动删除。另外通过free命令也可以看到shared内存占用情况。
2.基于POSIX规范mmap方式实现共享内存
mmap会将物理内存页通过页表全部映射到用户进程的虚拟内存页中。
3.通过memfd_create()和fd跨进程共享实现共享内存
memfd_create可以返回一个文件描述符,对共享内存的操作就像操作一个文件一样方便,但是这个文件是不体现在文件系统里的。这种方式可以理解为仅仅通过一个文件描述符即实现了内存的共享。
4.基于dma-buf的共享内存
DMA可以直接在内存和外设之间进行数据搬移,对于内存的存取来讲,外设和CPU一样,是一个访问master,可以直接访问内存。大名鼎鼎的kafka在底层的io传输就用到了DMA技术,加快了内存与网卡的传输速度。
python的共享内存
注:此文针对python3.8进行讨论。
python基于基于POSIX规范mmap方式实现共享内存。python处理共享内存的源码位于library/multiprocessing目录下,涉及到的文件有shared_memory.py。
在我们实际的使用场景中,多进程基本就两种模式:进程派生模式和无相关(进程间没有依赖关系)模式。对于python的派生模式还细分为fork和spawn两种方式。在fork模式和无相关模式下,因为python本身提供了内存垃圾回收的机制,在代码中一旦创建或者关联共享内存,会默认启动一个resource_tracker的跟踪子进程。如图所示,在fork模式下,每个进程都附加一个跟踪子进程。
当主进程退出后,垃圾回收会自动销毁共享内存空间。这样就会导致其他进程无法再次访问该共享内存的问题。这对于我们开发人员来说是不能接受的,共享内存的生命周期应该手动来管理。查看python的自动回收的代码,可以在shared_memory.py看到:
if _USE_POSIX:
# POSIX Shared Memory
if name is None:
while True:
name = _make_filename()
try:
self._fd = _posixshmem.shm_open(
name,
self._flags,
mode=self._mode
)
except FileExistsError:
continue
self._name = name
break
else:
name = "/" + name if self._prepend_leading_slash else name
self._fd = _posixshmem.shm_open(
name,
self._flags,
mode=self._mode
)
self._name = name
try:
if create and size:
os.ftruncate(self._fd, size)
stats = os.fstat(self._fd)
size = stats.st_size
self._mmap = mmap.mmap(self._fd, size)
except OSError:
self.unlink()
raise
from .resource_tracker import register
register(self._name, "shared_memory")
在linux环境下,无论是创建还是关联共享内存,都会在resource_tracker进程中进行注册。那么我们可以在自己的代码中调用unregister操作,取消自动跟踪。
from multiprocessing.resource_tracker import unregister
unregister('/' + shm.name, "shared_memory")
参考文章: