检查 Linux 系统上 python 多处理中的 fork 行为

Sam*_*ufi 3 python fork shared-memory multiprocessing

我必须从许多进程中访问一组大型且不可选取的 python 对象。因此,我想确保这些对象没有被完全复制。

根据这篇文章和这篇文章中的评论,对象不会被复制(在 UNIX 系统上),除非它们被更改。然而,引用一个对象将会改变它的引用计数,而引用计数又会被复制。

到目前为止这是正确的吗?由于我担心的是大型对象的大小,因此如果复制这些对象的小部分,我不会有问题。

为了确保我正确理解所有内容并且不会发生意外情况,我实现了一个小测试程序:

from multiprocessing import Pool

def f(arg):
    print(l, id(l), object.__repr__(l))
    l[arg] = -1
    print(l, id(l), object.__repr__(l))

def test(n):
    global l
    l = list(range(n))
    with Pool() as pool: 
        pool.map(f, range(n))
    print(l, id(l), object.__repr__(l))

if __name__ == '__main__':
    test(5) 
Run Code Online (Sandbox Code Playgroud)

在 的第一行中f,我希望id(l)在所有函数调用中返回相同的数字,因为列表在检查之前没有更改id

另一方面,在 的第三行中fid(l)应该在每个方法调用中返回不同的数字,因为列表在第二行中发生了更改。

然而,程序的输出让我感到困惑。

[0, 1, 2, 3, 4] 139778408436488 <list object at 0x7f20b261d308>
[-1, 1, 2, 3, 4] 139778408436488 <list object at 0x7f20b261d308>
[0, 1, 2, 3, 4] 139778408436488 <list object at 0x7f20b261d308>
[0, -1, 2, 3, 4] 139778408436488 <list object at 0x7f20b261d308>
[0, 1, 2, 3, 4] 139778408436488 <list object at 0x7f20b261d308>
[0, 1, -1, 3, 4] 139778408436488 <list object at 0x7f20b261d308>
[0, 1, 2, 3, 4] 139778408436488 <list object at 0x7f20b261d308>
[0, 1, 2, -1, 4] 139778408436488 <list object at 0x7f20b261d308>
[0, 1, 2, 3, 4] 139778408436488 <list object at 0x7f20b261d308>
[0, 1, 2, 3, -1] 139778408436488 <list object at 0x7f20b261d308>
[0, 1, 2, 3, 4] 139778408436488
Run Code Online (Sandbox Code Playgroud)

的所有呼叫和线路中的 id 都是相同的f。即使列表在末尾保持不变(如预期),情况也是如此,这意味着列表被复制。

如何查看对象是否已被复制?

Ros*_*dge 7

您的困惑似乎是由于误解流程和fork工作方式造成的。每个进程都有自己的地址空间,因此两个进程可以使用相同的地址而不会发生冲突。这也意味着一个进程无法访问另一个进程的内存,除非将相同的内存映射到两个进程中。

当进程调用fork系统调用时,操作系统会创建一个新的子进程,它是父进程的克隆。与任何其他进程一样,该克隆具有与其父进程不同的自己的地址空间。然而,地址空间的内容是父地址空间的精确副本。这过去是通过将父进程的内存复制到为子进程分配的新内存中来完成的。这意味着一旦子进程和父进程在fork任一进程对自己的内存进行任何修改后恢复执行,就不会影响另一个进程。

然而,复制进程的整个地址空间是一项昂贵的操作,并且通常是一种浪费。大多数时候,新进程立即执行一个新程序,这会导致子进程的地址空间被完全替换。因此,现代类 Unix 操作系统使用“写时复制”fork实现。不是复制父进程的内存,而是将父进程的内存映射到子进程中,以便它们可以共享相同的内存。然而,旧的语义仍然保留。如果子进程或父进程修改了共享内存,则修改的页面将被复制,以便两个进程不再共享该内存页面。

multiprocessing模块调用您的f函数时,它是在使用fork系统调用创建的子进程中执行的。由于该子进程是父进程的克隆,因此它还有一个名为 name 的全局变量l,该变量引用两个进程中具有相同 ID(地址)和相同内容的列表。也就是说,直到您修改l子进程中引用的列表。ID 不会(也不能)更改,但列表的子版本不再与父版本相同。父级列表的内容不影响子级所做的修改。

fork请注意,无论是否使用写时复制,上一段中描述的行为都是正确的。就multiprocessing模块和 Python 而言,这只是一个实现细节。无论如何,有效结果是相同的。这意味着您无法真正在 Python 程序中测试fork使用了哪种实现。