IT狗工作室Python中文社区

第3篇:CPython的内存模型架构-Layer 2

2020-06-07  本文已影响0人  铁甲万能狗

我们在前一篇的流程图中留下了两个黑箱子,会涉及到内存模型第一层以上的其他话题,回顾下面关于第一层面向类型的内存API流程执行图。本篇要讨论其中一个黑箱就是何为物?


那么,我们不妨将前一篇内存模型架构图和上面的内存函数接口执行流程图结合一起,我们可以得到一个更为清晰的CPython内存模型架构图,图中提到aranas和pool是本篇需要提及的难点,

Layer 1与Layer 2的内存APIs的交互

不过在深入了解这个CPython的内存策略前,我们需要引入两个CPython的专业术语,CPython根据内存分配的尺寸的阀值512字节可以分为,对Python对象做如下分类:

内存模型的第2层提到的PyObject_函数族,如下所示,它们位于Objects/obmalloc.c的第679行和第710行,具体的逻辑没必要好说,跟前篇提到内存函数接口是一致的。

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);
}

void
PyObject_GetArenaAllocator(PyObjectArenaAllocator *allocator)
{
    *allocator = _PyObject_Arena;
}

void
PyObject_SetArenaAllocator(PyObjectArenaAllocator *allocator)
{
    _PyObject_Arena = *allocator;
}

我们这里的重点是要遗留的一个关键问题的默认的Python内存分配器,遗留的一些代码细节,我们先看看代码细节pymalloc_alloc位于源文件Objects/obmalloc.c的第1608行开始开始的代码细节。见下图红色标出的一些C代码。


上面的代码细节大意逻辑

显然默认的Python内存分配器是直接驱动内存池,间接管理内存池的驱动函数。我们在代码中提取一些问题,它们就是本文提到的黑箱--Arenas/Pool内存池管理策略需要回答的一系列问题。

CPython的内存分配策略

CPython的内存管理策略,分3个不同级别的对象,分别是Arenas->pool->block,我先用一个思维导图,让你脑海中建立这三个对象的层次关系,读者可以先通过下图来初步理解这三个对象。这也是内存模型架构第2层中最为复杂堆内存托管逻辑。

Arenas->pool->block堆内存托管模型

块(Block)

CPython的内存管理策略中,首先定义逻辑上的“”,并且用8字节对齐的方式确定块的尺寸,换句话说块的尺寸可以看作8的倍数那么大,例如你创建来一个25字节的Python对象,25字节不是8字节的倍数,那么CPython运行时系统会根据内存对齐的原则为该Python对象额外添加7个填充字节,就凑够32字节(8的倍数),更明确地说,对于一个实际尺寸位于25~32字节这个区间的任意Python对象,都能放入一个32字节的逻辑块中

小型对象的内存块分配表

事实上,我们所说的块,它的基本单位是8个字节,而对于CPython语义中,有着不同尺寸的block。对于少于512字节的任意Python对象的内存尺寸的分配,不同内存尺寸有对应的按8字节对齐后的块尺寸对应,w如上表所示的第2列中的8的倍数称为size class(类型尺寸),每种size class(类型尺寸)都由一个索引与其对应,我们称这些索引是size class index,由于所有块的尺寸是8字节对齐,索引的计算公式非常简单.

块的类型尺寸索引=(已分配且对齐的内存尺寸 / 8)-1

这里用到C/C++内存对齐的基础知识,在Objects/obmalloc.c中由相关的源代码,例如由size class index转换为size class

/* Return the number of bytes in size class I, as a uint. */
#define INDEX2SIZE(I) (((uint)(I) + 1) << ALIGNMENT_SHIFT)

由size class 转换为 size class index

uint size = (uint)(nbytes - 1) >> ALIGNMENT_SHIFT;

池(Pools)

池封装大小相同的块,每个池的大小为4KB,可以查看源代码

/*
 * The system's VMM page size can be obtained on most unices with a
 * getpagesize() call or deduced from various header files. To make
 * things simpler, we assume that it is 4K, which is OK for most systems.
 * It is probably better if this is the native page size, but it doesn't
 * have to be.  In theory, if SYSTEM_PAGE_SIZE is larger than the native page
 * size, then `POOL_ADDR(p)->arenaindex' could rarely cause a segmentation
 * violation fault.  4K is apparently OK for all the platforms that python
 * currently targets.
 */

/*源文件Objects/obmalloc.c的第885行*/
#define SYSTEM_PAGE_SIZE        (4 * 1024)
#define SYSTEM_PAGE_SIZE_MASK   (SYSTEM_PAGE_SIZE - 1)
....
*
 * Size of the pools used for small blocks. Should be a power of 2,
 * between 1K and SYSTEM_PAGE_SIZE, that is: 1k, 2k, 4k.
 */

/*源文件Objects/obmalloc.c的第920行*/
#define POOL_SIZE               SYSTEM_PAGE_SIZE        /* must be 2^N */
#define POOL_SIZE_MASK          SYSTEM_PAGE_SIZE_MASK

每个池对象由一个叫struct pool_header的结构体来表示,源代码的Objects/obmalloc.c从938到948行的就是池头部定义

/* Pool for small blocks. */
struct pool_header {
    union { block *_padding;
            uint count; } ref;          /* number of allocated blocks    */
    block *freeblock;                   /* pool's free list head         */
    struct pool_header *nextpool;       /* next pool of this size class  */
    struct pool_header *prevpool;       /* previous pool       ""        */
    uint arenaindex;                    /* index into arenas of base adr */
    uint szidx;                         /* block size class index        */
    uint nextoffset;                    /* bytes to virgin block         */
    uint maxnextoffset;                 /* largest valid nextoffset      */
};

typedef struct pool_header *poolp;

其中重要的字段

其中理解struct pool_header的nextpool和prevpool字段这个较为简单,它们构成如下关于pool_header的双向链表:

理解单个内存池的难点就是如何根据freeblock和nextoffset和maxnextoffset字段来管理整个4KB的内存空间,因为pool的整个头部结构,我任你怎么打肿脸充胖子,也填充不满4KB空间的头8个字节,唯一肯定的是pool_header填充满8个字节会塞入一些填充字节位。那么CPython究竟如何去让pool_header的字段成员去描述4KB空间中,除了起始8个字节外的剩余内存空间?

CPython3.9的源代码文件Objects/obmalloc.c,没有定义一个独立的函数描述单个池初始化的过程,其中关于初始化pool_header结构体的代码位于allocate_from_new_pool(size)函数内部位于1571行到1578行,allocate_from_new_pool(size)是一个初始化arenas对象和pool_heade链表的函数,我们这里仅抽取其中相关的代码,封装在一个自定义的block* pool_init(poolp poop,uint szidx)函数内部,如下代码所示

#include <stdio.h>

#define uint    unsigned int
#define ALIGNMENT_SHIFT        3
#define ALIGNMENT              8               /* must be 2^N */
#define INDEX2SIZE(I) (((uint)(I) + 1) << ALIGNMENT_SHIFT)
#define _Py_SIZE_ROUND_UP(n, a) (((size_t)(n) + \
        (size_t)((a) - 1)) & ~(size_t)((a) - 1))
#define POOL_SIZE (4*1024)
#define POOL_OVERHEAD   _Py_SIZE_ROUND_UP(sizeof(struct pool_header), ALIGNMENT)

/* When you say memory, my mind reasons in terms of (pointers to) blocks */
typedef unsigned char block;
.....
typedef struct pool_header *poolp;

.....

poolp pool;

/*
* Initialize the pool header, set up the free list to
* contain just the second block, and return the first
* block.
*/
block* pool_init(poolp pool,uint szidx){
    block* bp;
    pool->szidx = szidx;
    size = INDEX2SIZE(szidx);
    bp = (block *)pool + POOL_OVERHEAD;
    pool->nextoffset = POOL_OVERHEAD + (size << 1);
    pool->maxnextoffset = POOL_SIZE - size;
    pool->freeblock = bp + size;
    *(block **)(pool->freeblock) = NULL;

    return bp;
}

我们这里假设内存池分配的block类型尺寸是16个字节,那么对应的size class index就是1,因此我们这里做一个初始化池的演算过程

当小型的Python对象不再有任何外部Python代码引用时,CPython的Arenas内存管理对象并没有立即将这些内存返回给操作系统,而是将这些用过的块空间标记为可用的,并在该小型Python对象释放后,将被标记为使用过(Used,请留意我这里使用的字眼)的块插入(链接)到pool对象头部的空闲块列表(freeblock)

下图是单个内存池的示意图,红色的表示已用的块,绿色标识闲置的块。

你可能会问,又闲置已使用,不是矛盾吗?我们看看内存池中块集合的三种状态,


对于上面的三种块集合的状态,只有空载状态会被链接到Arena对象的空闲池(freepools)链表

Arenas内存管理对象

arenas用于封装内存池,和内存池一样也有一个头部开销用于跟踪内存池链表,arenas对象有如下行为参数

查看CPython源代码Objects/obmalloc.c文件,如下

更新中.....

上一篇下一篇

猜你喜欢

热点阅读