IT狗工作室Python中文社区

第5篇CPython内存模型架构-Layer 2 - Arena

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

前言

现在开始时激动人心的时候了,因为我们重要要啃下整个内存模型中的难点之一Arenas对象,我们前一篇文章仅得讨论局限于单个内存池,当一个内存池满载的情况下,就有arenas对象为pymalloc_alloc分配其他可用的内存池。从源代码看来。arenas对象在CPython源代码中是由一个名为arena_object的结构体定义的。

//定义Arena的内存尺寸
#define ARENA_SIZE              (256 << 10)     /* 256KB */
.....

struct arena_object {
    //arena对象的地址,由malloc分配
    uintptr_t address;

    /* 指向下一个池的池对齐指针 */
    block* pool_address;

    /* arena对象内可用内存池的数量*/
    uint nfreepools;

    /* arena中的池总数(无论是否可用)*/
    uint ntotalpools;

    /* 可用池的单链接列表. */
    struct pool_header* freepools;

    /*
     * 只要此arena_object不与已分配的arena关联,
     * nextarena成员就用于链接单链接的“ unused_arena_objects”
     * 列表中所有未关联的arena_object。 
     * 在这种情况下,prevarena成员未使用。
     *
     * 当此arena_object与具有至少一个可用池的已分配arena相关联时,
     * 两个成员都在双向链接的“ usable_arenas”列表中使用,
     * 该列表按nfreepools值的升序进行维护。
     */
    struct arena_object* nextarena;
    struct arena_object* prevarena;
};



在Python整个进程的生命周期内,由一个静态指针变量arenas管理着所有arena对象所组成的数组,并且该变量指向该数组的第一个元素的内存地址。

/* 用于跟踪内存块(区域)的对象数组*/
static struct arena_object* arenas = NULL;

/*当前arenas数组中的arena对象的数量*/
static uint maxarenas = 0;

/*  arena_objects.未使用的arena对象的单链表头部*/
static struct arena_object* unused_arena_objects = NULL;


//与具有可用池的arenas关联的arena_object的双向链表,链表两端以NULL终止。
static struct arena_object* usable_arenas = NULL;

/* nfp2lasta[nfp] is the last arena in usable_arenas with nfp free pools */
static struct arena_object* nfp2lasta[MAX_POOLS_IN_ARENA + 1] = { NULL };

/* How many arena_objects do we initially allocate?
 * 16 = can allocate 16 arenas = 16 * ARENA_SIZE = 4MB before growing the
 * `arenas` vector.
 */
#define INITIAL_ARENA_OBJECTS 16

一个概念上的arena在Python源码中对应arena_object结构体,确切地说arena_object仅仅是arena的一部分,就像pool_header只是pool的一部分一样。完整的arena包括一个arena_object和透过这个arena_object管理的pool集合

究竟arenas对象是如何初始化的呢?我们看回pymalloc_alloc函数

arena对象的初始化

当初始化一个arena对象时,其初始化过程的函数栈的调用顺序如下图,new_arena是一个属于内存模型第2层的函数,PyMem_RawRealloc和_PyMem_RawRealloc都属于第1层的函数接口。第1层和第2层的函数接口我在前面的篇章已经说得很清楚


这里重点看一下struct arena_object* new_arena(void)函数,这里是不会讨论有关调试模式的arena内存分配,因为new_arena函数代码篇幅比较长,我们不妨分成将代码的上下文分成三段来分析。这里先看一下Objects/obmalloc.c的第1243行到1276行

#define INITIAL_ARENA_OBJECTS 16
....

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;
    
    //debug模式的相关代码不用理会
    if (debug_stats == -1) {
        const char *opt = Py_GETENV("PYTHONMALLOCSTATS");
        debug_stats = (opt != NULL && *opt != '\0');
    }
    if (debug_stats)
        _PyObject_DebugMallocStats(stderr);

    if (unused_arena_objects == NULL) {
        uint i;
        uint numarenas;
        size_t nbytes;

        /* 
          将每次分配的arena对象数量增加一倍
          每次新的arena对象内存,都需要必要内存溢出检测。
         */
        numarenas = maxarenas ? maxarenas << 1 : INITIAL_ARENA_OBJECTS;
        if (numarenas <= maxarenas)
            return NULL;                /* overflow */
#if SIZEOF_SIZE_T <= SIZEOF_INT
        if (numarenas > SIZE_MAX / sizeof(*arenas))
            return NULL;                /* overflow */
#endif
        /*向第1层API申请内存*/
        nbytes = numarenas * sizeof(*arenas);
        arenaobj = (struct arena_object *)PyMem_RawRealloc(arenas, nbytes);
        if (arenaobj == NULL)
            return NULL;
        arenas = arenaobj;
      .....
}

从上面的代码可以知道,每当下一次新的arena对象是批量申请的的内存分配量是前一次2倍,比如arena对象内存初始化默认就是的数量是16×sizeof(struct arena_object),意味着申请的堆内存空间是连续的,能够容纳16个arena_object结构体,下一次申请内存量就是32×sizeof(struct arena_object),意味着能够容纳32个arena_object结构体的指针。

如此类推如果当前内存分配量为N,那么下一次的内存分配量为2N。



备注:arenas本身是一个struct arena_object的指针,因此代码中的sizeof(*arenas)实际上等价于sizeof(struct arena_object)。

这里再看一下new_arena函数位于Objects/obmalloc.c的第1284行到1297行

#define INITIAL_ARENA_OBJECTS 16
....

static struct arena_object*
new_arena(void)
{
        .....
        /*
         只有当前arena中的所有页面(pool)都已满时,
        才会调用new_arena()。因此,没有指向旧数组的指针。
        因此,我们不必担心指针无效。当然可以增加一些断言:
         */
        assert(usable_arenas == NULL);
        assert(unused_arena_objects == NULL);

        /*将新的arena放在unused_arena_objects列表中*/
        for (i = maxarenas; i < numarenas; ++i) {
            arenas[i].address = 0;              /*标记为未关联*/
            arenas[i].nextarena = i < numarenas - 1 ?
                                   &arenas[i+1] : NULL;
        }

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

每次新增的arena对象数组会由静态变量arenas托管,并且新增的arena对象元素,都需要对每个新的arena对象的address字段做0初始化,这用于标识每个新增的arena对象未被关联

未被关联的arena对应的是unused_arena_objects指针

初次的arenas数组内存分配

需要注意的是每次内存分配新增的arena对象元素会,unused_arena_object指针始终都以指向&arenas[maxarenas]为目标,也即是随着整个arenas数组的增长会,unused_arena_object指针会跟随如下表maxarenas索引位置变化移动。

现在位于Objects/obmalloc.c的第1300行到1331行的代码

static struct arena_object*
new_arena(void)
{
    ......
    /* 
      将下一个可用的[arena]对象从列表的开头移开。
      源代码的第1300行到1302行
    */
    assert(unused_arena_objects != NULL);
    arenaobj = unused_arena_objects;
    unused_arena_objects = arenaobj->nextarena;
    /**/
    assert(arenaobj->address == 0);
    address = _PyObject_Arena.alloc(_PyObject_Arena.ctx, ARENA_SIZE);
    if (address == NULL) {
        /* The allocation failed: return NULL after putting the
         * arenaobj back.
         */
        arenaobj->nextarena = unused_arena_objects;
        unused_arena_objects = arenaobj;
        return NULL;
    }
    arenaobj->address = (uintptr_t)address;

    ++narenas_currently_allocated;
    ++ntimes_arena_allocated;
    if (narenas_currently_allocated > narenas_highwater)
        narenas_highwater = narenas_currently_allocated;
    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 = MAX_POOLS_IN_ARENA;
    excess = (uint)(arenaobj->address & POOL_SIZE_MASK);
    if (excess != 0) {
        --arenaobj->nfreepools;
        arenaobj->pool_address += POOL_SIZE - excess;
    }
    arenaobj->ntotalpools = arenaobj->nfreepools;

    return arenaobj;
}

当执行到位于第1300行到1302行,实际上就是将第一个arena对象从unused_arena_objects单链表中弹出,并且unused_arena_objects指针指向了arenas数组的第2个元素的内存地址(例如:0x55BAB2677860),意味着unused_arena_objects链表的首个元素就是arenas[1].

其中执行到1304行的源代码,从这条语句开始表明划出的arena对象开始初始化内存池,

address = _PyObject_Arena.alloc(_PyObject_Arena.ctx, ARENA_SIZE);

ARENA_SIZE这个宏其实表示ARENA的有效负载是256KB,一个pool的固定尺寸是4KB,而一个arena对象就托管着64个pool,这些概念清楚的话,那你就知道下面代码的用意。

查看第434行到440行的源代码,其中静态变量_PyObject_Arena变量根据系统平台选择C底层的堆内存分配器来执行初始化,对于Linux系统来说,这里就是_PyObject_ArenaMalloc,_PyObject_ArenaFree这两个函数指针

static PyObjectArenaAllocator _PyObject_Arena = {NULL,
#ifdef MS_WINDOWS
    _PyObject_ArenaVirtualAlloc, _PyObject_ArenaVirtualFree
#elif defined(ARENAS_USE_MMAP)
    _PyObject_ArenaMmap, _PyObject_ArenaMunmap
#else
    _PyObject_ArenaMalloc, _PyObject_ArenaFree
#endif
    };

那么上面的代码实际上等价于

static PyObjectArenaAllocator _PyObject_Arena = {
  NULL,
  _PyObject_ArenaMalloc,
  _PyObject_ArenaFree
    };

其中_PyObject_Arena的具体定义是PyObjectArenaAllocator类型,这种编程模式很熟悉吧,就是第一篇说过的描述内存块分配器的简单类,。其源代码位于Include/cpython/objimpl.h的第97行到106行

typedef struct {
    /* user context passed as the first argument to the 2 functions */
    void *ctx;

    /* allocate an arena of size bytes */
    void* (*alloc) (void *ctx, size_t size);

    /* free an arena */
    void (*free) (void *ctx, void *ptr, size_t size);
} PyObjectArenaAllocator;

那么说了那么多其实第1304行的语句实际上旧调用了_PyObject_ArenaMalloc函数接口来为arena对象分配额外256KB的空间,其底层就是对malloc的封装。

返回第一个arena对象时并且成功为64个pool分配内存,就是源代码从第1304行到1331行所做的事情,内存示意图如下:

allocate_from_new_pool函数

我们了解到arenas数组中的的第一个arena对象的内存分配以及该arena对象为其所托管的池集合的内存分配,所有这些细节后。现在将注意力返回到arena对象初始化过程中的allocate_from_new_pool函数,即在Objects/obmalloc.c源文件的第1453行到第1579行,来查看它运行时的内存状态

从第1466行可知即由new_arena函数返回从unused_arena_objects链表划出的arena对象。下面代码是第1466行-第1579行的代码。

static void*
allocate_from_new_pool(uint size)
{
    /* There isn't a pool of the right size class immediately
     * available:  use a free pool.
     */
    if (UNLIKELY(usable_arenas == NULL)) {
        /* No arena has a free pool:  allocate a new arena. */
#ifdef WITH_MEMORY_LIMITS
        if (narenas_currently_allocated >= MAX_ARENAS) {
            return NULL;
        }
#endif
      
        usable_arenas = new_arena();  //源代码的第1466行
        if (usable_arenas == NULL) {
            return NULL;
        }
        usable_arenas->nextarena = usable_arenas->prevarena = NULL;
        assert(nfp2lasta[usable_arenas->nfreepools] == NULL);
        nfp2lasta[usable_arenas->nfreepools] = usable_arenas;
    }
    assert(usable_arenas->address != 0);

    /* 
       arena已经具有最小的nfreepools值,因此减少nfreepools不会改变该值,
       并且我们不需要重新排列usable_arenas列表。 
       但是,如果arena完全被分配(池集合完全使用中),
       则需要从usable_arenas中删除其arena对象。
     */
    assert(usable_arenas->nfreepools > 0); //源代码行1481行
    if (nfp2lasta[usable_arenas->nfreepools] == usable_arenas) {
        /* It's the last of this size, so there won't be any. */
        nfp2lasta[usable_arenas->nfreepools] = NULL;
    }
    /* If any free pools will remain, it will be the new smallest. */
    if (usable_arenas->nfreepools > 1) {
        assert(nfp2lasta[usable_arenas->nfreepools - 1] == NULL);
        nfp2lasta[usable_arenas->nfreepools - 1] = usable_arenas;
    }

值得注意的是,全局struct arena_object类型的指针nfp2lasta在初始化时是一个包含65个NULL指针为元素的数组。

static struct arena_object* nfp2lasta[MAX_POOLS_IN_ARENA + 1] = { NULL };

nfplasta是一个用于记录usable_arenas双重链表具有空闲池(free pools)的最后一个可用arenas对象的数组,当然是记录arenas数组中元素对应的内存地址。

从第1466行执行到1481行的内存状态,如下图是初始化第一个arena对象后的内存状态,我们说usable_arenas指针负责维护一个双重链表,那么从下图可以看出

由于目前,我们分析的是CPython初始化第一个arena对象(arenas数组的第一个元素)时的内存状态,此时arenas对象中的freepools指针仍然为NULL,第1493行到1509行的if分支代码是目前不会被执行,意味值当前第一个arena对象的256KB内存空间还没有初始化任何内存池,我们这里将注意力转移到源代码文件的第1523行到1546行的代码

static void*
allocate_from_new_pool(uint size){
  ......
  else {
        /* 从内存池集合划出4KB的内存空间,用于初始化新的内存池. */
        assert(usable_arenas->nfreepools > 0);
        assert(usable_arenas->freepools == NULL);
        pool = (poolp)usable_arenas->pool_address;
        //内存池集合的有效内存区(边界)检测
        assert((block*)pool <= (block*)usable_arenas->address +
                                 ARENA_SIZE - POOL_SIZE);
        //计算当前内存池pool的索引值
        pool->arenaindex = (uint)(usable_arenas - arenas);
        //
        assert(&arenas[pool->arenaindex] == usable_arenas);
        pool->szidx = DUMMY_SIZE_IDX;
        //将当前可用的arena对象的pool_address指向下一个内存池的首个字节。
        usable_arenas->pool_address += POOL_SIZE;
        //递减当前arena对象的nfreepools计数器。
        --usable_arenas->nfreepools;

        /*
        如果当前usable_arenas指针所指向的arena对象的nfreepools计数器递减至0
        那么usable_arenas会指向usable_arenas链表的下一个arena对象
        */
        if (usable_arenas->nfreepools == 0) {
            //断定usable_arena指向usable_arenas链表的
            //下一个arena对象的条件
            assert(usable_arenas->nextarena == NULL ||
                   usable_arenas->nextarena->prevarena ==
                   usable_arenas);
            /* Unlink the arena:  it is completely allocated. */
            usable_arenas = usable_arenas->nextarena;
            if (usable_arenas != NULL) {
                usable_arenas->prevarena = NULL;
                assert(usable_arenas->address != 0);
            }
        }
    }
  ......
}

更新待续……

上一篇下一篇

猜你喜欢

热点阅读