无法从子进程访问内存映射(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,然后调用 MapViewOfFiledwNumberOfBytesToMap = 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)的主要内容,如果未能解决你的问题,请参考以下文章

Python进程间通信之共享内存

python - 如何从子进程中运行的ghostscript命令中捕获错误

无法在 python 3.8 中访问嵌套的 JSON

从子进程中实时捕获标准输出

Python多线程进程入门1

Linux进程通信 | 共享内存