Python内核阅读(二十一): 内存管理机制(上)

Python 2017-09-05

起步

内存管理, 对于编程语言来说是至关重要的一部分. 随着程序的运行, 会创建和销毁大量的对象. python提供了垃圾回收(GC)机制, 让用户从繁琐的手动维护内存的工作中.

内存管理架构

在python中, 会根据是否开启 Py_DEBUG 模式会有所不同, 在第一章中提过当开启debug模式, 所有的 PyObject 会组成一个双向列表. 这个模式在相关操作属于内存管理下额外的动作. 这一章, 关注的焦点是非debug模式下的内存管理机制.

在python中, 内存管理机制被抽象成具有层次类型的结构.

20170905151638.png

在最底层(第0层), 是 C 语言所提供的 mallocfree 接口, 属于操作系统提供的内存管理接口.

第一层是python基于第0层的管理接口的包装, 可以说并没有加入太多的动作, 其目的仅仅是为python提供一层统一的 raw memory 的管理接口. 这一层的作用就是处理与平台相关的内存分配行为. 对外的接口是一组以 PyMem_ 为前缀的函数族.

[obmalloc.c]
static void * _PyMem_RawMalloc(void *ctx, size_t size)
{
    if (size == 0)
        size = 1;
    return malloc(size);
}

static void _PyMem_RawFree(void *ctx, void *ptr)
{
    free(ptr);
}

void * PyMem_RawMalloc(size_t size)
{
    if (size > (size_t)PY_SSIZE_T_MAX)
        return NULL;
    return _PyMem_Raw.malloc(_PyMem_Raw.ctx, size);
}
...

我们看到, 在第一层, 可以就是等同于C的 malloc , realloc, free 的语义, 只是对与申请大小为0的内存做了处理, python不允许申请大小0的空间, 会强制转为申请1个字节的内存空间, 有趣的是, PyMem_Malloc 等还有宏:

[pymem.h]
#define PyMem_MALLOC(n)         PyMem_Malloc(n)
#define PyMem_REALLOC(p, n)     PyMem_Realloc(p, n)
#define PyMem_FREE(p)           PyMem_Free(p)

另外, 在这一层中, 还提供了面向python中类型的内存分配:

[pymem.h]
#define PyMem_New(type, n) \
  ( ((size_t)(n) > PY_SSIZE_T_MAX / sizeof(type)) ? NULL :  \
    ( (type *) PyMem_Malloc((n) * sizeof(type)) ) )
#define PyMem_NEW(type, n) \
  ( ((size_t)(n) > PY_SSIZE_T_MAX / sizeof(type)) ? NULL :  \
    ( (type *) PyMem_MALLOC((n) * sizeof(type)) ) )

#define PyMem_Resize(p, type, n) \
  ( (p) = ((size_t)(n) > PY_SSIZE_T_MAX / sizeof(type)) ? NULL :    \
    (type *) PyMem_Realloc((p), (n) * sizeof(type)) )
#define PyMem_RESIZE(p, type, n) \
  ( (p) = ((size_t)(n) > PY_SSIZE_T_MAX / sizeof(type)) ? NULL :    \
    (type *) PyMem_REALLOC((p), (n) * sizeof(type)) )

第一层所提供的内存管理接口功能十分有限, PyMem_New 中, 只需提供类型的数量, python会自动检测计算处所需要的内存空间大小. 但是在这层中, 只是简单的起到申请空间的作用, 初始化对象的引用计数等就没有, 因此, 需要在第一层的基础上抽象更高层次的管理接口.

第二层的内存管理结构以 PyObject_ 为前缀的函数族.

[obmalloc.c]
void * PyObject_Malloc(size_t size)
{
    /* see PyMem_RawMalloc() */
    if (size > (size_t)PY_SSIZE_T_MAX)
        return NULL;
    return _PyObject.malloc(_PyObject.ctx, size);
}

void * PyObject_Calloc(size_t nelem, size_t elsize)
{
    /* see PyMem_RawMalloc() */
    if (elsize != 0 && nelem > (size_t)PY_SSIZE_T_MAX / elsize)
        return NULL;
    return _PyObject.calloc(_PyObject.ctx, nelem, elsize);
}

void * PyObject_Realloc(void *ptr, size_t new_size)
{
    /* see PyMem_RawMalloc() */
    if (new_size > (size_t)PY_SSIZE_T_MAX)
        return NULL;
    return _PyObject.realloc(_PyObject.ctx, ptr, new_size);
}

void PyObject_Free(void *ptr)
{
    _PyObject.free(_PyObject.ctx, ptr);
}

小块空间的内存池

有时候应适当避免大量地执行 mallocfree 操作, 不然容易导致操作系统频繁地在用户态和内核态之间进行切换. 因此python引入了一个内存池机制, 用于管理对小块内存的申请和释放. 整个小块内存的内存池可以视为一个层次结构, 从下而上分别是 block, pool , arena内存池 4个层次.

Block

在最底层, block是一个确定大小的内存块. 在python中, 有很多种block, 不同种类的block都有不同的内存大小, 这些为了在当前主流的32位和64位平台上获得最佳的性能, 所有的block的长度都是8字节对齐的.

[obmalloc.c]
#define ALIGNMENT               8               /* must be 2^N */
#define ALIGNMENT_SHIFT         3

同时, block的大小也有上限, 当申请内存的大小小于这个闪现, python可以使用不同种类的block来满足对内存的需求; 当申请的内存大小超过的这个上限, python就会将内存的申请请求转交给第一层的内存管理机制, 即 PyMem_ 函数族来处理. 这个上限被设置为512:

[obmalloc.c]
#define SMALL_REQUEST_THRESHOLD 512
#define NB_SMALL_SIZE_CLASSES   (SMALL_REQUEST_THRESHOLD / ALIGNMENT)

根据 SMALL_REQUEST_THRESHOLDALIGNMENT 的限定, 实际上, 我们可以由此得到不同种类的block的size class分别为: 8, 16, 32, ..., 512. 每个size class对应一个size class index. 这个index从0开始, 所以小于512字节的小块内存的分配, 我们可以得到如下结论.

Request in bytes Size of allocated block Size class idx
1~8 8 0
9~16 16 1
17~24 24 2
... ... ...
505~512 512 63

也就是说, 当我们申请一块大小28字节的内存时, 实际上 PyObject_Malloc 从内存池中划分的内存是32字节的一个block, 从 size class index 为3的pool中划出. 下面的式子给出了在 size class 和 size class index之间的转换:

[obmalloc.c]
// 从size class index 转换到size class
#define INDEX2SIZE(I) (((uint)(I) + 1) << ALIGNMENT_SHIFT)

// size class 转换到size class index
size = (uint)(nbytes - 1) >> ALIGNMENT_SHIFT;

我们虽然谈论block, 但是在python中, block只是一个概念, 它不是PyListObject那样是有实体存在, 我们知道它是具有一定大小的内存, 但它不与python源码里的某个东西对应. 然而为了管理block, 就派生处pool来管理它.

pool

一组block集合成为一个pool. 换句话说, 一个pool管理着一堆有固定大小的内存块. 在python中, 一个pool的大小通常为一个系统内存页, 由于当前大多系统的内存也都是4KB, 所以python也将一个pool的大小定义为4KB:

[obmalloc.c]
#define SYSTEM_PAGE_SIZE        (4 * 1024)
#define SYSTEM_PAGE_SIZE_MASK   (SYSTEM_PAGE_SIZE - 1)

#define POOL_SIZE               SYSTEM_PAGE_SIZE        /* must be 2^N */
#define POOL_SIZE_MASK          SYSTEM_PAGE_SIZE_MASK

虽然block没有对应的结构, 但是pool却有:

[obmalloc.c]
typedef uint8_t block;

/* Pool for small blocks. */
struct pool_header {  
    union { block *_padding;  
            uint count; } ref;          //当前pool上面分配的block的数量  
    block *freeblock;                   //指向下一个可用的block,这里构成了一个链表, 它是一个离散的链表,很有意思  
    struct pool_header *nextpool;       //通过这两个指针形成pool的双链表  
    struct pool_header *prevpool;       /* previous pool        */  
    uint arenaindex;                    //在arena里面的索引  
    uint szidx;                         //分配内存的类别,8字节,16或者。。。  
    uint nextoffset;                    //下一个可用的block的内存偏移量  
    uint maxnextoffset;                 //最后一个block距离开始位置的距离  
}; 

typedef struct pool_header *poolp;

一个pool管理是block都是大小一致的. 不会存在一部分block是a个字节, 另一部分block是b个字节的. 这就是其成员 szindex 的意义, 它和size class index 联系在一起.

假设我们手中有一块4KB的内存, 如果将其改造为管理32字节block的pool, 并从中取出第一块block的:

[obmalloc.c _PyObject_Alloc]
typedef struct pool_header *poolp;
#define POOL_OVERHEAD   _Py_SIZE_ROUND_UP(sizeof(struct pool_header), ALIGNMENT)

block *bp;
poolp pool;
... // pool 指向一块4KB内存
pool->ref.count = 1;
size = INDEX2SIZE(size);// 设置pool的size class index, 比如3转化为32字节
// 跳过用于pool_header的内存, 并进行对齐
bp = (block *)pool + POOL_OVERHEAD;
// 实际就是pool->nextoffset = POOL_OVERHEAD + size + size;
pool->nextoffset = POOL_OVERHEAD + (size << 1);
pool->maxnextoffset = POOL_SIZE - size;
pool->freeblock = bp + size;
*(block **)(pool->freeblock) = NULL;
return (void *)bp;

最后返回的bp就是从pool中取出的第一块block的指针. 也就是说, pool中的第一块block已经被分配了, 所以在 ref.count 中国记录了当前已经被分配的数量, 这是为1. bp实际是一个地址, 这个地址后面将近4KB的内存都是可用的, 但是可用肯定的是申请内存的函数只会使用 [bp, bp + size] 这个区间的内存, 这是由于size class index保证的. 被改造的4KB的情况如图:

20170905152006.png

实线箭头是指针, 但是虚线表示的偏移量. nextoffsetmaxnextoffset 都是相对于pool头部的偏移位置.

假设从现在开始申请5块28字节的内存, 由于28对应32个字节, 所以实际上会申请5块32字节的内存:

[obmalloc.c _PyObject_Alloc]
if (pool != pool->nextpool) {
    ++pool->ref.count;
    bp = pool->freeblock;
    ...
    if (pool->nextoffset <= pool->maxnextoffset) {
        // 有足够的block空间
        pool->freeblock = (block*)pool +
                          pool->nextoffset;
        pool->nextoffset += INDEX2SIZE(size);
        *(block **)(pool->freeblock) = NULL;
        if (use_calloc)
            memset(bp, 0, nbytes);
        return (void *)bp;
    }
    ...
    return (void *)bp;
}

原来 freeblock 指向的是下一个可用的block的起始地址, 当再次申请32字节的block时, 只需返回freeblock指向的地址就可用了, 很显然, 这是freeblock需要向前进, 指向下一个可用的block, 这时, nextoffset 现身了. 在pool header中, nextoffset和maxoffset是两个用于pool的block集合进行迭代的变量: 从初始化中可用知道 freeblock之后的是下一个可用的block地址. 从这里分配block也可用看到, 在分配了block之后, freeblock和nextoffset都会向前移动一个block的距离. 当条件达到 pool->nextoffset > pool->maxnextoffset 就意味着已经遍历完pool中的所有block了.

所以申请5块32字节的内存就是, 申请, 前进, 申请, 前进... 可以想象pool中5个连续的block都被分配出去了. 过了一段时间, 程序释放了第二块和第四块的block, 那么下一次再分配32字节的内存时, 出于主观考虑, 我们是希望它能范湖第二块, 因为这样能提高pool的使用率. 可以想象, 一旦python运转后, 内存释放动作将会导致pool中出现大量离散的自由block, python得建立一个机制, 将这些离散的自由block组织起来, 再次使用. 这个机制就是所谓的自由 block 链表, 这个链表的关联就落在了pool_header中的那个freeblock身上.

我们知道, 当pool初始化完成后, freeblock指向一个有效的地址, 为下一个可以分配出去的block地址. 而奇特的是, python设置了freeblock还设置了*freeblock. 这一动作似乎非常诡异, 但这却是建立离散自由block链表的关键所在. 这个地方在block被释放的时候:

[obmalloc.c]
static void _PyObject_Free(void *ctx, void *p)
{
    poolp pool;
    block *lastfree;
    poolp next, prev;
    uint size;

    pool = POOL_ADDR(p);
    // 判断p指向的block是否属于pool
    if (address_in_range(p, pool)) {
        *(block **)p = lastfree = pool->freeblock;
        pool->freeblock = (block *)p;
        ...
    }
}

在释放block时, 这时 freeblock 虽然指向一个有效的pool内存地址, 但是 freeblock 是为 NULL 的. 假设这时python释放的是block A, 在代码 `(block *)p = lastfree = pool->freeblock;之后, A中的第一个字节的值被设置为了当前freeblock的值, 而(block **)p = lastfree = pool->freeblock;` 之后, freeblock的值就被更新了, 指向了block A的首地址. 短短两步, 就将离散自由的block链表建立起来, 真是了不起. 这时pool的结构就得到了更新, 如图:

20170905152059.png

讲到这里, 我们很容易以 freeblock = *freeblock 为线索遍历这条链表, 当 freeblock = NULL时, 则表明达到了链表的尾部了. 回头在看看申请block那边的代码就容易理解 *freeblock 了:

[obmalloc.c _PyObject_Alloc]
if (pool != pool->nextpool) {
    ++pool->ref.count;
    bp = pool->freeblock;
    if ((pool->freeblock = *(block **)bp) != NULL) {
        UNLOCK();
        if (use_calloc)
            memset(bp, 0, nbytes);
        return (void *)bp;
    }
    if (pool->nextoffset <= pool->maxnextoffset) {
        ...
    }
}    

在申请内存空间的时候, 优先去查找离散状态的block. 那还有一个问题, 如果这个pool中的block都被用光了呢? 最简单的解决方案是:找另一个pool, 这以为这, python中, 还需要一个pool集合.

arena

多个pool聚合的结果就是一个arene, pool的大小默认为4KB. 同样, 每个arena 的大小都有一个默认的值.

[obmalloc.c]
#define ARENA_SIZE              (256 << 10)     /* 256KB */

256KB, 那么, 一个arena中容纳的pool的个数就是 ARENA_SIZE / POOL_SIZE = 64 个. arena是个什么结构:

[obmalloc.c]
struct arena_object {
    uintptr_t address;
    block* pool_address;
    uint nfreepools;
    uint ntotalpools;

    struct pool_header* freepools;
    struct arena_object* nextarena;
    struct arena_object* prevarena;
};

一个概念上的 arena 在python对应了 arena_object 结构体. 从最后的两个成员看出, 它也是一个链表形式的存在. 和pool一样, 一个完整的pool包括一个pool_header和通过这个pool_header管理的block集合.

"未使用"的arena和"可用"的arena 在 arena_object 结构体通过 nextarenaprevarena 构成链表, 链表的表头就是 arenas. 而多个 arenas 也能构成集合, 这个集合是一个数组, 这个数组就是python中通用小块内存的内存池; 另一面, nextarenaprevarena 也确实是用来连接 arena_object 组成链表, 多个arena_object又通过数组组织起来, 有点稀奇古怪.

arena是用来管理一组pool的集合的, arena_object的作用看上去和pool_header的作用是一样的, 但还是有一点差别的, pool_header与其管理的pool是一块连续的内存; 而arena_object与其管理的内存则是分离的. 如图:

20170905152151.png

这点区别似乎没什么大不了的, 不就是分开吗. 但这背后居然隐藏着惊天的大咪咪秘密.

当pool_header申请时, 它所管理的block集合的内存一定也被申请了; 但是当申请arena_object时, 它所管理的pool集合的内存则没有被申请, 换句话说, arena_object和pool集合还需要建立关系.

当一个arena的arena_object没有与pool集合建立联系时, 这时的arena处于 "未使用" 状态; 建立关系后, arena就转为 "可用" 状态. 对每一个状态, 都有一个 arena 的链表. "未使用" 的arena的链表表头是 unused_arena_objects . 此时的arena与arena之间通过 nextarena 连接, 是一个单向链表; 而 "可用" 的arena的表头是 usable_arenas , arena 与 arena 通过 nextarenaprevarena 连接, 是一个双向链表, 如图展示了多个arena的可能状态:

20170905152224.png

申请 arena

在运行期间, python使用 new_arena 来创建一个 arena:

[obmalloc.c]
static struct arena_object* arenas = NULL;               // arenes 管理着 arena_object的集合
static uint maxarenas = 0;                               // 当前arenas管理着arena的个数
static struct arena_object* unused_arena_objects = NULL; // "未使用" arena_object 链表
static struct arena_object* usable_arenas = NULL;        // "可用" 的 arena_object 链表
#define INITIAL_ARENA_OBJECTS 16                         // 初始化时需要申请的arena_obejct的个数

static struct arena_object* new_arena(void)
{
    struct arena_object* arenaobj;
    uint excess;        /* number of bytes above pool alignment */
    void *address;
    static int debug_stats = -1;

    // 判断是否有可用的 
    if (unused_arena_objects == NULL) {
        uint i;
        uint numarenas;
        size_t nbytes;

        // 确定本次需要申请的 arena_object 的个数, 并申请内存
        numarenas = maxarenas ? maxarenas << 1 : INITIAL_ARENA_OBJECTS;
        if (numarenas <= maxarenas)     // 溢出
            return NULL;                /* overflow */

        nbytes = numarenas * sizeof(*arenas);
        arenaobj = (struct arena_object *)PyMem_RawRealloc(arenas, nbytes);
        if (arenaobj == NULL)
            return NULL;
        arenas = arenaobj;

        // 初始化申请的 arena_object, 并放入 unused_arena_objects 中.
        for (i = maxarenas; i < numarenas; ++i) {
            arenas[i].address = 0;              /* mark as unassociated */
            arenas[i].nextarena = i < numarenas - 1 ?
                                   &arenas[i+1] : NULL;
        }

        /* Update globals. */
        unused_arena_objects = &arenas[maxarenas];
        maxarenas = numarenas;
    }

    // 从 unused_arena_objects 链表中取出一个"未使用" 的arena_object
    arenaobj = unused_arena_objects;
    unused_arena_objects = arenaobj->nextarena;

    // 申请arena_object管理的内存
    address = _PyObject_Arena.alloc(_PyObject_Arena.ctx, ARENA_SIZE);
    arenaobj->address = (uintptr_t)address;
    ++narenas_currently_allocated;
    ++ntimes_arena_allocated;
    if (narenas_currently_allocated > narenas_highwater)
        narenas_highwater = narenas_currently_allocated;
    // 设置pool集合的相关信息
    arenaobj->freepools = NULL;
    /* pool_address <- first pool-aligned address in the arena
       nfreepools <- number of whole pools that fit after alignment */
    arenaobj->pool_address = (block*)arenaobj->address;
    arenaobj->nfreepools = ARENA_SIZE / POOL_SIZE;

    // 将pool的起始地址调整为系统页的边界
    excess = (uint)(arenaobj->address & POOL_SIZE_MASK);
    if (excess != 0) {
        --arenaobj->nfreepools;
        arenaobj->pool_address += POOL_SIZE - excess;
    }
    arenaobj->ntotalpools = arenaobj->nfreepools;

    return arenaobj;
}

首先会检查 unused_arena_objects 链表中是否还有 "未使用" 的arena, 检查结果决定后续动作. 如果 unused_arena_objects 中存在未使用的arena, 那么直接从中取出一个arena, 调整unused_arena_objects指针, 与抽取的arena断绝联系. python申请一块大小为 ARENA_SIZE (256KB)的内存, 将申请的内存地址复制给arena的address.这个地址就是维护pool的集合用的. 这块256KB的内存就是pool的容身之处. 这时, arena_object和pool集合建立了联系, 具备了 "可用" 的条件, 就等着被 usable_arenas 收入后宫了.

内存池

可用pool缓冲池--usedpools

python中, 祛痱粉小块内存和大块内存是分界点是 512个字节, 这是宏 SMALL_REQUEST_THRESHOLD 设定的. 也就是说, 当申请的内存小于512个字节, PyObject_Malloc会在内存池中申请内存; 但当申请的内存大于512个字节时, 则用 malloc 来申请.

内存池中的pool在运行的任何时刻, 总处于三个状态中的一种:

  • used状态: pool中至少有一个block被使用, 并至少有一个block未被使用
  • full: pool中所有的block都被使用, 这种状态的pool在arena中, 当不在arena的freepools链表中.
  • empty: pool中所有的block都未被使用, 处于这个状态的pool的集合通过其pool_header中的nextpool构成一个链表, 这个链表的表头就是arena_object中的freepools.

如图给出了一个arena中包含三种状态pool的集合:

20170905152257.png

注意, full状态的poool是个字独立的, 并没有像其他pool那样会链接成链表. 所有处于 used 状态的pool都被置于 usedpools 的控制下. usedpools 与size class index 有着密切的联系, 来看一看usedpools的结构:

[obmalloc.c]
#define PTA(x)  ((poolp )((uint8_t *)&(usedpools[2*(x)]) - 2*sizeof(block *)))
#define PT(x)   PTA(x), PTA(x)

static poolp usedpools[2 * ((NB_SMALL_SIZE_CLASSES + 7) / 8) * 8] = {
    PT(0), PT(1), PT(2), PT(3), PT(4), PT(5), PT(6), PT(7)
#if NB_SMALL_SIZE_CLASSES > 8
    , PT(8), PT(9), PT(10), PT(11), PT(12), PT(13), PT(14), PT(15)
...
#endif
}

NB_SMALL_SIZE_CLASSES 就是 #define NB_SMALL_SIZE_CLASSES (SMALL_REQUEST_THRESHOLD / ALIGNMENT) 通过计算它的值是64, 这个指明了当前配置下, 一共有多少个 size class.

假设我们手中有一个size class为32字节的pool, 要将其放入这个usedpools中时, 需要怎么做? 从上面的描述可以看到, 只需要 usedpools[i+i]->nextpool = pool 即可. 其中i为size class index. 对应32字节, i = 3.

PyObject_Malloc 中, Python利用的usedpools巧妙结构, 通过简单的判断来发现与某个class size index 对应的pool是否在usepools中存在:

[obmalloc.c PyObject_Malloc]
if ((nbytes - 1) < SMALL_REQUEST_THRESHOLD) {
    LOCK();
    size = (uint)(nbytes - 1) >> ALIGNMENT_SHIFT; // 获得size class index
    pool = usedpools[size + size];
    if (pool != pool->nextpool) {   // usedpools中可用的pool
    ...

Pool 的初始化

当python启动之后, 在usedpools这个小块空间内存池中, 并不存在任何可用的内存, 即不存在任何可用的pool. 当开始申请小块内存时, python才建立这个内存池. 我们申请32字节的内存, python首先找到对应的class size index(3) 在usedpools对应的位置查找, 发现没有任何可用的pool, python会从useable_arenas链表中第一个可用的arena获得一个可用的pool. 考虑到, 这个pool将是用于分配32字节block的, 因此在此它需要被重新划分.

初始化之一 这部分的初始化叫做 init_pool :

[obmalloc.c]
static void * _PyObject_Alloc(int use_calloc, void *ctx, size_t nelem, size_t elsize)
{
    ...
init_pool:
    // 将pool放入usedpools中
    next = usedpools[size + size];
    pool->nextpool = next;
    pool->prevpool = next;
    next->nextpool = pool;
    next->prevpool = pool;
    pool->ref.count = 1;
    // pool在之前就具有正确的size结构, 直接返回pool中的一个block
    if (pool->szidx == size) {
        bp = pool->freeblock;
        assert(bp != NULL);
        pool->freeblock = *(block **)bp;
        UNLOCK();
        if (use_calloc)
            memset(bp, 0, nbytes);
        return (void *)bp;
    }
    // 初始化pool_header, 将freeblock指向第二个block, 返回第一个block
    pool->szidx = size;
    size = INDEX2SIZE(size);
    bp = (block *)pool + POOL_OVERHEAD;
    pool->nextoffset = POOL_OVERHEAD + (size << 1);
    pool->maxnextoffset = POOL_SIZE - size;
    pool->freeblock = bp + size;
    *(block **)(pool->freeblock) = NULL;
    UNLOCK();
    if (use_calloc)
        memset(bp, 0, nbytes);
    return (void *)bp;

    ...

}

Python将得到的pool放入了usedpools中. 那么在什么情况下pool从empty转为used状态呢? 假设申请的内存为size class index 为 i. 字儿usedpools[i + i] 处没有处于used状态的pool. 同时全局变量freepools中海油处于empty的pool, 那么位于freepool维护的链表中头部pool将被取出来, 放入usedpools中, 这时, 这个pool也就从empry状态转为used状态:

[obmalloc.c _PyObject_Alloc]
pool = usable_arenas->freepools;
if (pool != NULL) {
    usable_arenas->freepools = pool->nextpool;
    ...// 调整usable_arenas->nfreepools和usable_arenas自身

init_pool:
    ...
}

初始化之二 PyObject_Malloc 从 new_arena 中国获得一个新的arena后, 是怎样来初始化其中的pool集合, 并最终完成分配一个block的最终任务的呢, 一步一步来:

[obmalloc.c _PyObject_Alloc]
pool = usable_arenas->freepools; // 从arena中取出一个崭新的pool
pool->arenaindex = (uint)(usable_arenas - arenas);
pool->szidx = DUMMY_SIZE_IDX;
usable_arenas->pool_address += POOL_SIZE;
--usable_arenas->nfreepools;
...
goto init_pool;

python从arena取出pool后, 设置pool的arenaindex, 这个index实际上就是pool所在的arena位于arenas所指的数组中的序号, 随后pool的szidx设置为0xffff, 以表示这个pool从来没管理过block集合. 接着调整useable_arenas.

无论什么开源的项目, 内存管理都是最繁琐, 最能体现细节是魔鬼的地方, 在申请内存的过程中, 还有一些细节这里就不一一涉及了.

block 释放

根据前面对pool的介绍, 可以推测出, 对block释放是可能会对pool的状态发生变化:

  • used状态转变为empty状态
  • full状态转变为used状态

更多情况是释放block后, pool状态还是used状态. 这是最简单的情况, 我们先从最简单的情况说起:

[obmalloc.c]
static void _PyObject_Free(void *ctx, void *p)
{
    poolp pool;
    block *lastfree;
    poolp next, prev;
    uint size;
    _Py_AllocatedBlocks--;
    pool = POOL_ADDR(p);
    if (address_in_range(p, pool)) {
        LOCK();
        // 设置离散自由的block链表
        *(block **)p = lastfree = pool->freeblock;
        pool->freeblock = (block *)p;
        if (lastfree) {// lastfree有效, 表明当前pool不是出于full状态
            struct arena_object* ao;
            uint nf;  /* ao->nfreepools */
            if (--pool->ref.count != 0) {// 不需要转换为empty状态
                UNLOCK();
                return;
            }
            ...
        }
    }
    ...
}

这种情况下的block, 将它重新放入自由block链表中, 并调整pool中的ref.count这个引用计数, 比较简单.

那如果释放block, 会改变pool的状态呢? 比如, 从full到used. 这种情况下就是将pool重新链会到 usedpools 中即可:

[obmalloc.c _PyObject_Free]
if (address_in_range(p, pool)) {
    ...
    // 当前pool处于full状态, 释放一块block后, 需将其转化为used状态
    // 并重新链如usedpools的头部
    --pool->ref.count;
    assert(pool->ref.count > 0);            /* else the pool is empty */
    size = pool->szidx;
    next = usedpools[size + size];
    prev = next->prevpool;
    /* insert pool before next:   prev <-> pool <-> next */
    pool->nextpool = next;
    pool->prevpool = prev;
    next->prevpool = pool;
    prev->nextpool = pool;
    UNLOCK();
    return;
}

最复杂的情况是pool从used状态转为empty. empty的pool需要重新回到freepools中去:

[obmalloc.c]
static void _PyObject_Free(void *ctx, void *p)
{
    poolp pool;
    block *lastfree;
    poolp next, prev;
    uint size;

    pool = POOL_ADDR(p);
    if (address_in_range(p, pool)) {
        LOCK();
        *(block **)p = lastfree = pool->freeblock;
        pool->freeblock = (block *)p;
        if (lastfree) {
            struct arena_object* ao;
            uint nf;  /* ao->nfreepools */
            ...
            // 将pool放入freepools维护的链表中
            ao = &arenas[pool->arenaindex];
            pool->nextpool = ao->freepools;
            ao->freepools = pool;
            nf = ++ao->nfreepools;
            ...
        }
        ...
    }
    ...
}

将empty状态的pool放入freepools维护的链表中, 这样就可以了, 一切都能正确运转了.

但是但是, 这会引起一个问题. 那就是python从来都没真正free任何pool. 就是始终都没有调用c语言的 free 来进行内存释放. 那么arena能用多少内存呢. 默认情况下, 限制内存池的宏 WITH_MEMORY_LIMITS 是没有开启的, 也就是说arena想用多少就用多少. 你可能会问, 这有什么关系呢?

有这样一种情况, 就是在某一时刻需要申请一个比较大的内存160MB. arena会满足你的需求. 过了一段时间, 我不需要160MB了, 我将它释放, 但是python仅仅只是将其归还给arena. 因为arena从来不会释放其维护的pool集合. 所以这160MB的内存始终被python占用着. 这是不是就很浪费了.

这种情况下, 当程序某个时刻需要大量内存后, python的占用内存飙升后就再也掉不下来了.

为了解决这个问题, 就希望arena能将自己自己维护的pool集合释放, 返回给操作系统, 从而必须从"可用"状态转为"未使用"状态, 这也是arena有两个状态的原因. 在前面的代码之后, Python处理完pool, 就开始处理arena了.

对arena的处理实际上分为4中情况.

1. 当arena中所有的pool都是empty, 释放pool集合占用的内存

[obmalloc.c]
// 将pool放入freepools维护的链表中
ao = &arenas[pool->arenaindex];
pool->nextpool = ao->freepools;
ao->freepools = pool;
nf = ++ao->nfreepools;
if (nf == ao->ntotalpools) {
    // 调整usable_arenas链表
    if (ao->prevarena == NULL) {
        usable_arenas = ao->nextarena;
    }else {
        ao->prevarena->nextarena = ao->nextarena;
    }

    if (ao->nextarena != NULL) {
        ao->nextarena->prevarena = ao->prevarena;
    }
    // 调整unused_arena_objects链表
    ao->nextarena = unused_arena_objects;
    unused_arena_objects = ao;
    // 释放内存
    _PyObject_Arena.free(_PyObject_Arena.ctx, (void *)ao->address, ARENA_SIZE);
    ao->address = 0;                        /* mark unassociated */
    --narenas_currently_allocated;
    UNLOCK();
    return;
}

可以蛋刀, 除了将arena维护的pools的内存归还给系统之外, python还调整了usable_arenas和unused_arena_object链表, 将arena的状态转到了"未使用"状态.

2. 如果之前arena中都不是empty状态, 那现在就变成有一个是empty pool, 将该arena链如到usable_arenas链表的表头

3. 若arena中empty 的pool数量为n, 则从useable_arenas开始薛兆arena可以插入的位置, 目的是将usable_arenas变成有序的链表, 这样, 如果一个arena的empty pool数量越多, 它被使用的机会就越少. 这样的arena就更有机会被free

4. 其他情况, 不进行任何对arena的处理

内存池全景

python内存管理可能是最复杂最繁琐的部分了, 我们只要记住, 所有申请的内存全都在 arenas 中:

20170905152351.png


本文由 hongweipeng 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

如果对您有用,您的支持将鼓励我继续创作!