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

Checking fork behaviour in python multiprocessing on Linux systems

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

根据 this and this post 中的评论,对象不会被复制(在 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) 

f 的第一行中,我希望 id(l) 到 return 在所有函数调用中使用相同的数字,因为列表在 [=16= 之前没有更改] 检查。

另一方面,在 f 的第三行中,id(l) 应该 return 在每个方法调用中使用不同的数字,因为列表在第二行中发生了变化。

但是,程序输出让我很困惑。

[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

f的所有调用和线路中的id相同。即使列表在最后保持不变(正如预期的那样),情况也是如此,这意味着列表 has 已被复制。

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

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

当进程调用 fork 系统调用时,操作系统会创建一个新的 child 进程,它是 parent 进程的克隆。这个克隆,像任何其他进程一样,有它自己的地址 space 不同于它的 parent。但是地址 space 的内容是 parent 的精确副本。这过去是通过将 parent 进程的内存复制到分配给 child 的新内存来实现的。这意味着一旦 child 和 parent 在 fork 之后恢复执行,任何一个进程对自己的内存所做的任何修改都不会影响另一个。

但是,复制进程的整个地址 space 是一项昂贵的操作,而且通常是一种浪费。大多数情况下,新进程会立即执行新程序,导致 child 的地址 space 被完全替换。因此,现代 Unix-like 操作系统使用 "copy-on-write" fork 实现。不是复制 parent 进程的内存,而是 parent 的内存被映射到 child 中,因此它们可以共享相同的内存。然而,旧的语义仍然被保留。如果 child 或 parent 修改共享内存,则复制修改的页面,以便两个进程不再共享该内存页面。

multiprocessing 模块调用您的 f 函数时,它会在使用 fork 系统调用创建的 child 进程中执行此操作。由于这个 child 进程是 parent 的克隆,它还有一个名为 l 的全局变量,它指的是两个进程中具有相同 ID(地址)和相同内容的列表.也就是说,直到您在 child 过程中修改 l 引用的列表。 ID 不会(也不能)更改,但 child 的列表版本不再与 parent 的相同。 parent 列表的内容不受 child.

所做修改的影响

请注意,无论 fork 是否使用 copy-on-write,上一段中描述的行为都是正确的。就 multiprocessing 模块和 Python 一般而言,这只是一个实现细节。不管怎样,有效的结果都是一样的。这意味着您无法在 Python 程序中真正测试使用了 fork 实现。