无法从子进程访问内存映射(Python 3.8)
Posted
技术标签:
【中文标题】无法从子进程访问内存映射(Python 3.8)【英文标题】:Can't access memory map from child process (Python 3.8) 【发布时间】:2020-09-04 00:17:42 【问题描述】:我正在编写一个程序,该程序使用 Python 的 multiprocessing
模块来加速 CPU 密集型任务,并且我希望我创建的子进程能够访问最初在父进程中创建的内存映射,而无需复制它。根据multiprocessing
documentation,从 Python 3.4 开始,子进程默认不再继承文件描述符,所以我尝试使用os.set_inheritable()
来覆盖该行为。
这是我为演示该问题而制作的快速模型:
DATA = r"data.csv"
from sys import platform
WINDOWS = platform.startswith("win")
import os
from multiprocessing import Process
import mmap
from typing import Optional
def child(fd: int, shm_tag: Optional[str]) -> None:
if shm_tag: # i.e. if using Windows
mm = mmap.mmap(fd, 0, shm_tag, mmap.ACCESS_READ)
else:
mm = mmap.mmap(fd, 0, mmap.MAP_SHARED, mmap.PROT_READ)
mm.close()
if __name__ == "__main__":
# Some code differs on Windows
WINDOWS = platform.startswith("win")
# Open file
fd = os.open(DATA, os.O_RDONLY | os.O_BINARY if WINDOWS else os.O_RDONLY)
os.set_inheritable(fd, True)
# Create memory map from file descriptor
if WINDOWS:
shm_tag = "shm_mmap"
mm = mmap.mmap(fd, 0, shm_tag, mmap.ACCESS_READ)
else:
shm_tag = None
mm = mmap.mmap(fd, 0, mmap.MAP_SHARED, mmap.PROT_READ)
# Run child process
(p := Process(target = child, args = (fd, shm_tag), daemon = True)).start()
p.join()
p.close()
mm.close()
os.close(fd)
这根本不起作用,或者至少在我主要测试的 Windows* 上不起作用。我在子进程中收到一个错误,这严重暗示文件描述符实际上并未被继承:
Process Process-1:
Traceback (most recent call last):
File "C:\Program Files\Python38\lib\multiprocessing\process.py", line 315, in _bootstrap
self.run()
File "C:\Program Files\Python38\lib\multiprocessing\process.py", line 108, in run
self._target(*self._args, **self._kwargs)
File "C:\Users\[N.D.]\Documents\test.py", line 12, in child
mm = mmap.mmap(fd, 0, shm_tag, mmap.ACCESS_READ)
ValueError: cannot mmap an empty file
此外,无论我将True
还是False
传递给os.set_inheritable()
,我都会得到完全相同的错误,就好像它实际上并没有什么不同。
发生了什么事?我是否错误地使用了mmap
模块?
* 可能相关:Windows 使用spawn()
而不是fork()
创建新进程,如果您尝试内存映射一个空文件,则会引发异常。
【问题讨论】:
multiprocessing
通过subprocess.Popen
生成工作进程,而不继承句柄。它依赖于句柄的显式复制。即使它确实继承了 handles,子进程也不会使用私有 CRT 协议来继承 C 文件描述符。您必须从msvcrt.get_osfhandle
传递句柄,然后在子进程中通过msvcrt.open_osfhandle
将其包装在文件描述符中。
一种解决方法,因为您正在命名文件映射,所以通过mmap.mmap(-1, size, shm_tag, mmap.ACCESS_READ)
在工作人员中按名称打开它。在这种情况下,您需要准确的 size
,因为 WinAPI CreateFileMappingW
需要一个源,如果大小作为 0 传递,系统使用它来查询实际大小。这是 mmap 模块的限制。在 Windows C API 中,您将调用 OpenFileMappingW
,然后调用 MapViewOfFile
和 dwNumberOfBytesToMap = 0
。
顺便说一句,您需要一个唯一的实例化名称,例如 f'appname_shm_mmap_os.getpid()'
,因为当前会话中的所有标准进程(非沙盒)都为命名内核对象共享相同的本地命名空间。
非常感谢您的帮助!我现在已经开始工作了。
【参考方案1】:
感谢 Eryk Sun 的 cmets,我能够进行有效的实施:
DATA = r"data.csv"
from sys import platform
if platform.startswith("win"):
WINDOWS = True
from msvcrt import get_osfhandle
else:
WINDOWS = False
import os
from multiprocessing import Process
import mmap
from typing import Optional
def child(fd_or_size: int, shm_tag: Optional[str]) -> None:
if WINDOWS:
mm = mmap.mmap(-1, fd_or_size, shm_tag, mmap.ACCESS_READ)
else:
mm = mmap.mmap(fd_or_size, 0, mmap.MAP_SHARED, mmap.PROT_READ)
mm.close()
if __name__ == "__main__":
# Open file
fd = os.open(DATA, os.O_RDONLY | os.O_BINARY if WINDOWS else os.O_RDONLY)
# Create memory map from file descriptor
if WINDOWS:
# Obtain underlying file handle from file descriptor
os.set_handle_inheritable(get_osfhandle(fd), True)
shm_tag = f"test_mmap_os.getpid()"
mm = mmap.mmap(fd, 0, shm_tag, mmap.ACCESS_READ)
else:
os.set_inheritable(fd, True)
mm = mmap.mmap(fd, 0, mmap.MAP_SHARED, mmap.PROT_READ)
# Run child process
(p := Process(target = child, args = (mm.size() if WINDOWS else fd, shm_tag),
daemon = True)).start()
p.join()
p.close()
mm.close()
os.close(fd)
重要更改(全部在 Windows 上):
使用get_osfhandle()
从文件描述符中获取底层文件句柄
为内存映射指定一个特定于进程的标记名
在子进程中,通过提供已知的映射大小来附加到内存映射。
【讨论】:
这里不需要os.set_handle_inheritable(get_osfhandle(fd), True)
。当前的实现依赖于在 Windows 中按名称打开文件映射。此外,请记住 multiprocessing Process
不会为 Windows 中的衍生进程启用继承。它手动将句柄复制到子进程。以上是关于无法从子进程访问内存映射(Python 3.8)的主要内容,如果未能解决你的问题,请参考以下文章