美文网首页IT狗工作室Python中文社区
第5篇CPython内存模型架构-Layer 2 - Arena

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

作者: 铁甲万能狗 | 来源:发表于2020-06-15 19:20 被阅读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指针负责维护一个双重链表,那么从下图可以看出

    • usable_arenas双链表:NULL↔arenas[0]↔NULL
    • unused_arena_objects单链表:arenas[1]→arenas[2]→....→arenas[15]→NULL

    由于目前,我们分析的是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);
                }
            }
        }
      ......
    }
    

    更新待续……

    相关文章

      网友评论

        本文标题:第5篇CPython内存模型架构-Layer 2 - Arena

        本文链接:https://www.haomeiwen.com/subject/tacitktx.html