Python垃圾回收和Linux Fork

dongdong醬發表於2022-01-13

前言

在口袋助理看到了其他部門的同事針對Python2記憶體佔用做的一點優化工作,自己比較感興趣,遂記錄下。

Linux fork簡介

fork是Linux提供的建立子程式的系統呼叫。為了優化建立程式速度,Linux核心使用了Copy-on-Write的方式去建立程式,所謂Copy-on-Write是指執行fork之後,
核心並不立即給子程式分配實體記憶體空間,而是讓子程式的虛記憶體對映到父程式的實體記憶體。僅僅當子程式向地址空間中執行寫入操作時,才給它分配一段實體記憶體。
通過這種方式既優化了程式建立的時間,又減少了子程式的記憶體佔用。

Copy-On-Write策略增加Python多程式記憶體佔用的原因

Python GC採用引用技術的方式去管理對每個物件的引用,每一個被GC跟蹤的物件會由一個PyGC_Head的結構體去表示。如下所示,其中gc_refs就是每個物件的引用計數值,
當我們在子程式中讀取父程式建立的物件的時候,就會導致子程式的虛地址空間中的gc_refs加1,從而觸發了核心的缺頁中斷,這是核心就會給子程式建立新的實體記憶體。
僅僅是簡單的讀取操作就會導致新的記憶體空間產生。

/* GC information is stored BEFORE the object structure. */
typedef union _gc_head 
{
    struct {
        union _gc_head *gc_next;
        union _gc_head *gc_prev;
        Py_ssize_t gc_refs;
    } gc;
    long double dummy; /* force worst-case alignment */
} PyGC_Head;

解決辦法

python3的解決方法

針對這個問題,Python3.7增加了三組API(有instagram團體提交的)[1]。

freeze用於將GC追蹤的所有物件都移動到永生代(permanent generation),之後垃圾回收會忽略這些被設定為永生代的物件。

實際使用中,我們可以在父程式中執行freeze函式,然後子程式中使用和父程式共享的物件,這樣物件的引用技術就不會增加,從而避免了COW的發生。

python2的解決方法

(1) 針對Python2,我們可以簡單的把Python3的相關函式移植過來

(2) 使用multiprocessing.Array去共享資料。Array會從共享記憶體中取一段取儲存資料,並不會增加引用技術值,從而觸發COW。
實現方面,Array使用Posix共享記憶體 + mmap去實現。[3]

#!/usr/bin/env python
# coding=utf-8
from multiprocessing import Array
import os
import sys

def foo():
    shared_cache = Array('i', range(0, 100), lock=False)
    pid = os.fork()
    if pid > 0:
        print("parent:", sys.getrefcount(shared_cache)) 
    elif pid == 0:
        print("child:", sys.getrefcount(shared_cache))


foo()

參考

1.https://instagram-engineering.com/copy-on-write-friendly-python-garbage-collection-ad6ed5233ddf
2.https://llvllatrix.wordpress.com/2016/02/19/python-vs-copy-on-write/
3.https://github.com/python/cpython/blob/main/Lib/multiprocessing/shared_memory.py

相關文章