第3篇:CPython的内存模型架构-Layer 2
我们在前一篇的流程图中留下了两个黑箱子,会涉及到内存模型第一层以上的其他话题,回顾下面关于第一层面向类型的内存API流程执行图。本篇要讨论其中一个黑箱就是何为物?
- 首先PyMem_这些函数族,在逻辑上是CPython内存模型架构的第1层,
- 再次,_PyObject_函数族一个衔接第1层和第2层的,衔接函数接口
- pymalloc_alloc函数压根就不是分配器(不知道为何官方冠以默认分配器之名),更确切地说是一个调度函数,将来自外部CPython其他内部对象的内存空间请求是往第2层还是往第1层转发,显然当需要分配大于512字节时,调用前上图提到的PyMem_Raw前缀的函数族。
那么,我们不妨将前一篇内存模型架构图和上面的内存函数接口执行流程图结合一起,我们可以得到一个更为清晰的CPython内存模型架构图,图中提到aranas和pool是本篇需要提及的难点,
Layer 1与Layer 2的内存APIs的交互不过在深入了解这个CPython的内存策略前,我们需要引入两个CPython的专业术语,CPython根据内存分配的尺寸的阀值512字节可以分为,对Python对象做如下分类:
-
大于512字节的Python对象,称为大型对象(Big),而Arenas对象的尺寸为256KB就是CPython中大型对象因此Arenas对象的内存分配,CPython会选择调用PyMem_RawMalloc()或PyMem_RawRealloc()为其分配内存,换句话就是通过第0层去调用C库的malloc分配器,因此C底层的malloc分配器是仅供给arenas对象使用的。
-
少于或等于512字节的Python对象,称为小型对象(Small),小型对象的内存请求按该对象的类型尺寸分组,这些分组按8个字节对齐,由于返回的地址必须有效对齐。这些类型尺寸的对象的内存请求由4KB的内存池提供内存分配,当然前提是该内存池有闲置的块。
内存模型的第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代码。
上面的代码细节大意逻辑
- 第一步:检索数组usepools中与申请的内存尺寸量相关的某个usepool元素,就是我们在上文插图(Layer 1与Layer 2的内存APIs的交互) 提到的pool,
- 第二步:在池中找到可用的内存块(bp=pool->freeblock),若找到旧返回该内存块,若找不到池中空闲的内存块就执行pymalloc_pool_extend函数。
- 第三步:若第一步中连可用的pool(第1612行)都找不到,就执行 allocate_from_new_pool函数
显然默认的Python内存分配器是直接驱动内存池,间接管理内存池的驱动函数。我们在代码中提取一些问题,它们就是本文提到的黑箱--Arenas/Pool内存池管理策略需要回答的一系列问题。
- 第1609行的 usedpools是什么?poolp是什么数据类型?
- 第1610行的block是数据类型?
- 函数pymalloc_pool_extend(pool,size)的具体逻辑是什么?
- allocate_from_new_pool(size)的具体逻辑是什么?
CPython的内存分配策略
CPython的内存管理策略,分3个不同级别的对象,分别是Arenas->pool->block,我先用一个思维导图,让你脑海中建立这三个对象的层次关系,读者可以先通过下图来初步理解这三个对象。这也是内存模型架构第2层中最为复杂堆内存托管逻辑。
Arenas->pool->block堆内存托管模型-
每个Arenas对象包装包含64个内存池,每个Arenas固定大小为256KB,并且该对象头部用两个struct area_object类型的指针在堆中构成Arenas对象的双重链表。
-
每个内存池(Pool),固有尺寸为4KB,每个内存池包含尺寸相同的逻辑块,并且并且该对象头部用两个struct pool_header类型的指针构成pool对象的双重链表。
-
块是封装Python对象的基本单位,对于Areas对象来说都按8字节的块来划分PyMem已分配的所有堆内存(备注:切入点1)。
块(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;
其中重要的字段
- ref表示已分配内存块的数量
- freeblock):标识下一次分配block的起始位置
- nextpool和prevpool:它们是struct pool_header类型的指针分别指向当前池对象下一个内存池和上一个内存池,和其他池构成一个双重链表,这样,即使在不同的池中,该算法也可以轻松找到给定块类型大小的可用空间。
- arenaindex:表示当前pool所属的arena的索引
- nextoffset: 表示freeblock的下一次偏移的位置
- maxnextoffset:表示当前pool最后一个block未知距离pool的偏移,这意味当nextoffset大于maxnextoffset时,就没有可分配的block了
其中理解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,因此我们这里做一个初始化池的演算过程
- pool->szidx=1,标识这个池用于分配16字节block的内存池
- INDEX2SIZE(size)是一个将size class idx转换为size class的宏,此时局部变量size=16
- 宏POOL_OVERHEAD表示4KB空间中,将当前池头部信息满整个起始8个字节的空间。
当小型的Python对象不再有任何外部Python代码引用时,CPython的Arenas内存管理对象并没有立即将这些内存返回给操作系统,而是将这些用过的块空间标记为可用的,并在该小型Python对象释放后,将被标记为使用过(Used,请留意我这里使用的字眼)的块插入(链接)到pool对象头部的空闲块列表(freeblock)
下图是单个内存池的示意图,红色的表示已用的块,绿色标识闲置的块。
你可能会问,又闲置又已使用,不是矛盾吗?我们看看内存池中块集合的三种状态,
- 使用过:块集合既不为闲置的块也有已使用的块(红色的),并且当前已使用的块至少一个以上
- 满载:表示当前没有可用的块
- 空载:表示每个块都是空的,
对于上面的三种块集合的状态,只有空载状态会被链接到Arena对象的空闲池(freepools)链表
Arenas内存管理对象
arenas用于封装内存池,和内存池一样也有一个头部开销用于跟踪内存池链表,arenas对象有如下行为参数
- 其他抽象的内存管理对象将使用arena对象对象的空间
- 仅当arena对象的所有池为空载时,CPython将该对象占用的内存空间释返还给操作系统。
查看CPython源代码Objects/obmalloc.c文件,如下
更新中.....