第5篇CPython內(nèi)存模型架構(gòu)-Layer 2 - Arena對象

前言

現(xiàn)在開始時激動人心的時候了似芝,因為我們重要要啃下整個內(nèi)存模型中的難點之一Arenas對象蛹磺,我們前一篇文章僅得討論局限于單個內(nèi)存池锹安,當一個內(nèi)存池滿載的情況下,會由arenas對象為pymalloc_alloc調(diào)用返回其他可用的內(nèi)存池并闲。

我們先看看arenas對象在CPython源代碼中是由一個名為arena_object的結(jié)構(gòu)體定義的细睡。

//定義Arena的內(nèi)存尺寸
#define ARENA_SIZE              (256 << 10)     /* 256KB */
.....

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

    /* 指向可用內(nèi)存池的對齊指針(首個字節(jié)) */
    block* pool_address;

    /* arena對象托管256KB空間中可用的內(nèi)存池的數(shù)量*/
    uint nfreepools;

    /* arena中的內(nèi)存池總數(shù)(無論是否可用)*/
    uint ntotalpools;

    /* 可用池的單鏈接列表. */
    struct pool_header* freepools;

    /*
     * 只要此arena_object不與已分配的arena關(guān)聯(lián)帝火,
     * nextarena成員就用于鏈接單鏈接的“ unused_arena_objects”
     * 列表中所有未關(guān)聯(lián)的arena_object溜徙。 
     * 在這種情況下,prevarena成員未使用犀填。
     *
     * 當此arena_object與具有至少一個可用池的已分配arena相關(guān)聯(lián)時蠢壹,
     * 兩個成員都在雙向鏈接的“ usable_arenas”列表中使用,
     * 該列表按nfreepools值的升序進行維護九巡。
     */
    struct arena_object* nextarena;
    struct arena_object* prevarena;
};

在Python整個進程的生命周期內(nèi)图贸,由一系列的靜態(tài)指針變量來跟蹤每個arena對象的狀態(tài),比如arenas變量管理著所有arena對象所組成的數(shù)組,并且該變量指向該數(shù)組的第一個元素的內(nèi)存地址冕广。其他的靜態(tài)變量見如下代碼所示

/* 用于跟蹤內(nèi)存塊(區(qū)域)的對象數(shù)組*/
static struct arena_object* arenas = NULL;

/*當前arenas數(shù)組中的arena對象的數(shù)量*/
static uint maxarenas = 0;

/*  arena_objects.未使用的arena對象的單鏈表頭部*/
static struct arena_object* unused_arena_objects = NULL;


//與具有可用池的arenas關(guān)聯(lián)的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對象的初始化

接下來佳窑,我們需要知道的是CPython在進程生命周期開始時,是如何初始化我們的arena對象父能,即重講述CPython為arena對象分配內(nèi)存神凑,初始化arena對象各個字段、以及使用相關(guān)全局變量跟蹤arenas數(shù)組中各個arena對象的狀態(tài)何吝。執(zhí)行python命令時溉委,python內(nèi)部默認會連續(xù)初始化7個arena對象,如下圖所示


要窺探python內(nèi)部的運行時的內(nèi)存分配的細節(jié)爱榕,簡單粗暴的方法就是在Objects/obmalloc.c源碼文件的相關(guān)上下文插入printf函數(shù)瓣喊,然后再編譯cpython源代碼后執(zhí)行。

每當初始化一個arena對象(包含內(nèi)存分配和字段賦值)時黔酥,其初始化過程的函數(shù)棧的調(diào)用順序如下圖藻三,new_arena是一個屬于內(nèi)存模型第2層的函數(shù),PyMem_RawRealloc和_PyMem_RawRealloc都屬于第1層的函數(shù)接口跪者。第1層和第2層的函數(shù)接口我在前面的篇章已經(jīng)說得很清楚棵帽。


這里重點講解第一個arena對象初始化的過程≡幔看一下struct arena_object* new_arena(void)函數(shù),這里是不會討論有關(guān)調(diào)試模式的arena內(nèi)存分配逗概,因為new_arena函數(shù)代碼篇幅比較長,我們不妨分成將代碼的上下文分成三段來分析忘衍。這里先看一下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模式的相關(guān)代碼不用理會
    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對象數(shù)量增加一倍
          每次新的arena對象內(nèi)存逾苫,都需要必要內(nèi)存溢出檢測卿城。
         */
        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申請內(nèi)存*/
        nbytes = numarenas * sizeof(*arenas);
        arenaobj = (struct arena_object *)PyMem_RawRealloc(arenas, nbytes);
        if (arenaobj == NULL)
            return NULL;
        arenas = arenaobj;
      .....
}

從上面的代碼可以知道,arena對象的內(nèi)存分配是作為一個動態(tài)數(shù)組的連續(xù)元素的內(nèi)存尺寸向C底層的realloc函數(shù)申請的铅搓。每當下一次新的arena對象是批量申請的的內(nèi)存分配量是前一次2倍瑟押,比如arena對象內(nèi)存初始化默認就是的數(shù)量是16×sizeof(struct arena_object),意味著申請的堆內(nèi)存空間是連續(xù)的,能夠容納16個arena_object結(jié)構(gòu)體狸吞,下一次申請內(nèi)存量就是32×sizeof(struct arena_object)勉耀,意味著能夠容納32個arena_object結(jié)構(gòu)體。

如此類推如果當前內(nèi)存分配量為N蹋偏,那么下一次的內(nèi)存分配量為2N便斥。



備注:arenas本身是一個struct arena_object的指針,因此代碼中的sizeof(*arenas)實際上等價于sizeof(struct arena_object)威始。

這里再看一下new_arena函數(shù)位于Objects/obmalloc.c的第1284行到1297行枢纠,下面代碼對arenas數(shù)組的某些字段以及unused_arena_objects變量執(zhí)行初始化

#define INITIAL_ARENA_OBJECTS 16
....

static struct arena_object*
new_arena(void)
{
        .....
        /*
         只有當前arena中的所有頁面(pool)都已滿時,
        才會調(diào)用new_arena()黎棠。因此晋渺,沒有指向舊數(shù)組的指針。
        因此脓斩,我們不必擔心指針無效木西。當然可以增加一些斷言:
         */
        assert(usable_arenas == NULL);
        assert(unused_arena_objects == NULL);

        /*將新的arena放在unused_arena_objects列表中*/
        for (i = maxarenas; i < numarenas; ++i) {
            arenas[i].address = 0;              /*標記為未關(guān)聯(lián)*/
            arenas[i].nextarena = i < numarenas - 1 ?
                                   &arenas[i+1] : NULL;
        }

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

每次新增的arena對象數(shù)組會由靜態(tài)變量arenas托管,并且新增的arena對象元素随静,都需要對每個新的arena對象的address字段做0初始化八千,這用于標識每個新增的arena對象未被關(guān)聯(lián)

未關(guān)聯(lián)的arena對象,意味著該arena對象還沒被使用,由unused_arena_objects指針以一個單向鏈表的形式負責跟蹤其狀態(tài)燎猛,如下圖所示恋捆。

初次的arenas數(shù)組內(nèi)存分配

需要注意的是每次內(nèi)存分配新增的arena對象元素,unused_arena_object指針始終都以指向&arenas[maxarenas]為目標重绷,也即是隨著整個arenas數(shù)組的增長會,unused_arena_object指針會跟隨如下表maxarenas索引位置變化移動沸停。

現(xiàn)在看一下位于Objects/obmalloc.c的第1300行到1331行的代碼

static struct arena_object*
new_arena(void)
{
    ......
    /* 
      對應源代碼的第1300行到1302行
      將下一個可用的[arena]對象從unused_aren_objects列表的開頭移出。
    */
    assert(unused_arena_objects != NULL);
    arenaobj = unused_arena_objects;
    unused_arena_objects = arenaobj->nextarena;
    /**/
    assert(arenaobj->address == 0);
    //對應源代碼第1304行
    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;
}

當執(zhí)行到位于第1300行到1302行昭卓,實際上就是將第一個arena對象從unused_arena_objects單鏈表中彈出愤钾,并且unused_arena_objects指針指向了arenas數(shù)組的第2個元素的內(nèi)存地址(例如:0x55BAB2677860),意味著從unused_arena_objects鏈表移出的arenas[0]元素最終會交由usable_arenas指針所接管(下文會談?wù)摰?.

其中執(zhí)行到1304行的源代碼,從這條語句開始表明劃出的arena對象開始初始化內(nèi)存池候醒,

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

ARENA_SIZE這個宏其實表示ARENA的有效負載是256KB绰垂,一個pool的固定尺寸是4KB,而一個arena對象就托管著64個pool火焰,這些概念清楚的話劲装,那你就知道下面代碼的用意。

查看第434行到440行的源代碼,其中靜態(tài)變量_PyObject_Arena變量根據(jù)系統(tǒng)平臺選擇C底層的堆內(nèi)存分配器來執(zhí)行初始化,對于Linux系統(tǒng)來說占业,這里就是_PyObject_ArenaMalloc,_PyObject_ArenaFree這兩個函數(shù)指針

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類型,這種編程模式很熟悉吧绒怨,就是第一篇說過的描述內(nèi)存塊分配器的簡單類,。其源代碼位于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行的語句實際上舊調(diào)用了_PyObject_ArenaMalloc函數(shù)接口來為arena對象分配額外256KB的空間谦疾,其底層就是對malloc的封裝南蹂。

返回第一個arena對象時并且成功為64個pool分配內(nèi)存,就是源代碼從第1304行到1331行所做的事情念恍,內(nèi)存示意圖如下:

allocate_from_new_pool函數(shù)

我們了解到arenas數(shù)組中的的第一個arena對象的內(nèi)存分配以及該arena對象為其所托管的池集合的內(nèi)存分配六剥,所有這些細節(jié)后。現(xiàn)在將注意力返回到arena對象初始化過程中的allocate_from_new_pool函數(shù)峰伙,即在Objects/obmalloc.c源文件的第1453行到第1579行,來查看它初始化后的第一個arena對象的運行時的內(nèi)存狀態(tài)

從第1466行可知即由new_arena函數(shù)返回的是從unused_arena_objects鏈表劃出的arena對象疗疟。下面代碼是第1466行-第1521行的代碼

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已經(jīng)具有最小的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;
    }

     /* 
      嘗試獲取一個閑置的內(nèi)存池音榜,由于usable_arenas->freepools為NULL
       if語句塊對應源代碼文件的第1494行到1521行的代碼是不會被執(zhí)行的.
     */
    poolp pool = usable_arenas->freepools;
    if (LIKELY(pool != NULL)) {
        /* Unlink from cached pools. */
        usable_arenas->freepools = pool->nextpool;
        usable_arenas->nfreepools--;
        if (UNLIKELY(usable_arenas->nfreepools == 0)) {
            /* Wholly allocated:  remove. */
            assert(usable_arenas->freepools == NULL);
            assert(usable_arenas->nextarena == NULL ||
                   usable_arenas->nextarena->prevarena ==
                   usable_arenas);
            usable_arenas = usable_arenas->nextarena;
            if (usable_arenas != NULL) {
                usable_arenas->prevarena = NULL;
                assert(usable_arenas->address != 0);
            }
        }
        else { 
            assert(usable_arenas->freepools != NULL ||
                   usable_arenas->pool_address <=
                   (block*)usable_arenas->address +
                       ARENA_SIZE - POOL_SIZE);
        }
    }
    else {
        /* 
        目前會執(zhí)行該語句塊的代碼庞瘸,對應源代碼的第154行到1545行
        從內(nèi)存池集合劃出4KB的內(nèi)存空間,用于初始化新的內(nèi)存池. */
        assert(usable_arenas->nfreepools > 0);
        assert(usable_arenas->freepools == NULL);
        pool = (poolp)usable_arenas->pool_address;
        //內(nèi)存池集合的有效內(nèi)存區(qū)(邊界)檢測
        assert((block*)pool <= (block*)usable_arenas->address +
                                 ARENA_SIZE - POOL_SIZE);
        //計算當前內(nèi)存池pool的索引值
        pool->arenaindex = (uint)(usable_arenas - arenas);
        
        assert(&arenas[pool->arenaindex] == usable_arenas);
        //DUMMY_SIZE_IDX,目前搞不懂用意何在?
        pool->szidx = DUMMY_SIZE_IDX;
        //將當前可用的arena對象的pool_address指向下一個內(nèi)存池的首個字節(jié)赠叼。
        usable_arenas->pool_address += POOL_SIZE;
        //遞減當前arena對象的nfreepools計數(shù)器擦囊。
        --usable_arenas->nfreepools;

        /*
        對應源代碼的第1535行的代碼到1544行的代碼

        如果當前usable_arenas指針所指向的arena對象的nfreepools大于0
        當前if語句內(nèi)的代碼并沒執(zhí)行,對應源代碼的第1535行到1545行梅割。
        */
        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);
            }
        }
    }
  .....
}

值得注意的是霜第,全局struct arena_object類型的指針nfp2lasta在初始化時是一個包含65個NULL指針為元素的數(shù)組葛家。

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

nfplasta是一個用于記錄usable_arenas雙重鏈表具有空閑池(free pools)的最近一個可用arena對象户辞,當然是記錄arenas數(shù)組中元素對應的內(nèi)存地址。

從上面代碼可知癞谒,對于開辟用于初始化的arenas[0]中的第一個內(nèi)存池來說底燎,內(nèi)存狀態(tài)如下圖,目前第1535行的代碼到1544行的代碼是不會被執(zhí)行的,我后文會在再提及弹砚。我這里先給執(zhí)行到源代碼的第1466行到1533行的內(nèi)存示意圖,這是CPython初始化第一個arena對象以及初始化其托管256KB空間中低地址端中的第一個內(nèi)存池的狀態(tài)双仍。如下圖是初始化第一個arena對象后的內(nèi)存狀態(tài),我們說usable_arenas指針負責維護一個雙重鏈表桌吃,那么從下圖可以看出

  • usable_arenas雙鏈表:NULL?arenas[0]?NULL
  • unused_arena_objects單鏈表:arenas[1]→arenas[2]→....→arenas[14]→NULL

了解CPython初始化第一個arena對象以及初始化其托管256KB空間中低地址端中的第一個內(nèi)存池的狀態(tài)過程朱沃,我認為很重要,這是整個arena級別到pool級別內(nèi)存對象分配的起點《何铮基于目前往上其他同類文章的CPython內(nèi)存模型的源碼解讀搬卒,沒有這個詳細的過程描述,那么我這篇文章是他們內(nèi)容最好的補充翎卓。

在CPython主循環(huán)的驅(qū)動下契邀,arenas數(shù)組中7個arenas對象初始化過程是一個非常復雜的過程。不僅包含每個aren對象托管的內(nèi)存池集合中pool的內(nèi)存分配和初始化失暴。還有包含CPython相關(guān)加載模塊的其他內(nèi)部Python對象的內(nèi)存分配至各個已初始化的內(nèi)存池中坯门。為簡化該過程,伴隨arena初始化其他內(nèi)部Python對象內(nèi)存分配這里不做討論逗扒。

接下來從第一個arena對象到第七個arena對象在初始化過程中古戴,我們抽取一些特殊細節(jié)做些說明。

在第一個arena對象托管的內(nèi)存池集合空間(256KB)里缴阎,初始化第一個內(nèi)存池后允瞧,如下圖如此類推...第2個、第3個蛮拔、......你們發(fā)現(xiàn)到什么細節(jié)呢述暂?

  • 首先、我們說同一個arena對象所托管的不同內(nèi)存池建炫,這些已初始化的內(nèi)存池具有不同的size class idx畦韭,也就是同一個arena對象托管的不同內(nèi)存池的size class idx是不一樣的,它們的變化根據(jù)外部CPython代碼的內(nèi)存請求而定,例如內(nèi)存池這一時刻的size class idx是3,下一個時刻在內(nèi)存池的塊都被回收后肛跌,size class idx可能變?yōu)?1.

  • 然后艺配、nf2lasta,你們會發(fā)現(xiàn)每次調(diào)用allocate_from_new_pool(uint size),全局靜態(tài)數(shù)組的nf2lasta的下標變化和當前usable_arenas所指向的arena對象(此時是arenas數(shù)組中的第一個元素arenas[0])中的nfreepools計數(shù)器是實時同步的,nf2lasta數(shù)組其實是整個arenas數(shù)組狀態(tài)的跟蹤器衍慎,通過其下標和及其引用追蹤到usable_arenas當前所指向的arena對象转唉。

arena對象后續(xù)初始化

上面再來看看當前arena所托管的內(nèi)存池集合滿載狀態(tài)(fulled)的情景,也就是說內(nèi)存池集合所有內(nèi)存池的狀態(tài)都是正在使用(using)稳捆,見如下圖

此時的情景是第一個arena對象所托管內(nèi)池集合中的最后一個內(nèi)存池的內(nèi)存地址赠法,這里假設(shè)是140628899726336,也就是當前arenas[0]所托管的內(nèi)存池集合區(qū)域的末端邊界的地址為140628899730432,而當前arenas[0]的pool_address所指向的地址剛好也是該內(nèi)存地址乔夯。我們這里先拋出一個問題砖织。

pool_address指針偏移問題:可以這么認為后續(xù)的每個arena對象所托管的內(nèi)存池集合,他們都位于一塊連續(xù)的堆內(nèi)存空間嗎?
答案:是否定的末荐,我們在下面講解第2個后續(xù)的arena初始化時會印證一點侧纯。事實上我們要指出的是arena對象的pool_address指針是指向池集合空間內(nèi)下一個要初始化的內(nèi)存池的起始邊界,隨著初始化到第64個內(nèi)存池,pool_address指針就指向第64個內(nèi)存池末端的邊界

在CPython啟動時甲脏,主循環(huán)自第二次調(diào)用allocate_from_new_pool(uint size)眶熬,跟蹤arenas的幾個全局靜態(tài)變量已經(jīng)發(fā)生變化妹笆,此時與arena對象有關(guān)的全局靜態(tài)計數(shù)器的

  • unused_arena_objects!=NULL
  • usable_arenas=NULL
  • narenas_currently_allocated=1
  • ntimes_arena_allocated=1

意味著allocate_from_new_pool調(diào)用new_arena函數(shù)內(nèi)部的代碼上下文和第一次發(fā)生變化,如下代碼所示,這個代碼我不需要再解析娜氏。

static struct arena_object*
new_arena(void)
{
    //由于unused_arena_objects并不為NULL,第一個if分支會跳過
    if (unused_arena_objects == NULL) {
      ....
    }
        /* Take the next available arena object off the head of the list. */
    assert(unused_arena_objects != NULL);
    arenaobj = unused_arena_objects;
    unused_arena_objects = arenaobj->nextarena;
    assert(arenaobj->address == 0);
    //為當前arena對象分配256KB的內(nèi)存池集合
    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;
    //遞增各個靜態(tài)的計數(shù)器
    ++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;
}

現(xiàn)在先來看一下是第二個arena對象其內(nèi)存池集合中第一個內(nèi)存池初始化后的情景圖晾浴,我們查看一下如下圖第二個內(nèi)存池的地址邊界,和上面圖例的第一個內(nèi)存池集合的邊界是不一樣的牍白。每個arena對象托管的內(nèi)存池集合的堆內(nèi)存區(qū)域都是分散在堆內(nèi)存中脊凰。

usable_arenas雙鏈表

請讀者細心想一下,usable_arenas這個雙鏈表在第一個arena對象滿載后,偏移至第二個arenas對象茂腥,換句話說狸涌,當一個arena對象它所托管的內(nèi)存池集合中的所有內(nèi)存池的狀態(tài)都正在使用(using),那么CPython會將該arena對象從usable_arenas鏈表中刪除最岗,源代碼見Objects/obmalloc.c的第1498行到1509行

static void*
allocate_from_new_pool(uint size)
{
        ....
        if (usable_arenas->nfreepools == 0) {
            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);
            }
        }
        ....
}

最后帕胆,在CPython的7個arena對象初始化完成后,它們托管的內(nèi)存池集合都完全分配了般渡,如下圖所示

小結(jié)

上面的所有圖例都是從源代碼上下文插入各個關(guān)鍵位置插入printf函數(shù)懒豹,在編譯執(zhí)行后,根據(jù)打印出來的關(guān)鍵信息繪制出來的驯用。最大程度上呈現(xiàn)了CPython運行時的內(nèi)存模型脸秽。你會發(fā)現(xiàn)我多次強調(diào)不需要理會CPython源代碼中和Debug模式相關(guān)的代碼,因為那是在CPython編譯后給IDE調(diào)試時查看內(nèi)存信息所使用的蝴乔。由于通過IDE查看的信息可能在堆和棧中參雜了不同程度的調(diào)試信息或調(diào)試時內(nèi)存計數(shù)器记餐。這樣你通過IDE查看的內(nèi)存數(shù)據(jù)繪制內(nèi)存模型圖例,可能和CPython運行時出現(xiàn)差異薇正。

因此片酝,我鼓勵把你自己的大腦當成一個IDE編譯器,用你的大腦來理解CPython源代碼挖腰。而不是通過IDE雕沿,只要你多嘗試自己去閱讀,并且對一些比較晦澀難懂的代碼猴仑,插入一些測試性代碼审轮,用一些簡單的數(shù)據(jù)做運行時演算。時刻記得宁脊,printf函數(shù)是你的好朋友断国。有助于提高你對CPython源代碼的理解贤姆。

這里遺留一些問題榆苞,源代碼的第1548行到1565行的內(nèi)容有些操作涉及usedpool(內(nèi)存池緩沖陣列),我打算留到下一篇再補充

  //源代碼的1548行到1565行
  /* Frontlink to used pools. */
    block *bp;
    poolp next = usedpools[size + size]; /* == prev */
    pool->nextpool = next;
    pool->prevpool = next;
    next->nextpool = pool;
    next->prevpool = pool;
    pool->ref.count = 1;
    if (pool->szidx == size) {
        /* Luckily, this pool last contained blocks
         * of the same size class, so its header
         * and free list are already initialized.
         */
        bp = pool->freeblock;
        assert(bp != NULL);
        pool->freeblock = *(block **)bp;
        return bp;
    }

更新待續(xù)……

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末霞捡,一起剝皮案震驚了整個濱河市坐漏,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖赊琳,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件街夭,死亡現(xiàn)場離奇詭異,居然都是意外死亡躏筏,警方通過查閱死者的電腦和手機板丽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來趁尼,“玉大人埃碱,你說我怎么就攤上這事∷峙ⅲ” “怎么了砚殿?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長芝囤。 經(jīng)常有香客問我似炎,道長,這世上最難降的妖魔是什么悯姊? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任羡藐,我火速辦了婚禮,結(jié)果婚禮上悯许,老公的妹妹穿的比我還像新娘传睹。我一直安慰自己,他們只是感情好岸晦,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布欧啤。 她就那樣靜靜地躺著,像睡著了一般启上。 火紅的嫁衣襯著肌膚如雪邢隧。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天冈在,我揣著相機與錄音倒慧,去河邊找鬼。 笑死包券,一個胖子當著我的面吹牛纫谅,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播溅固,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼付秕,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了侍郭?” 一聲冷哼從身側(cè)響起询吴,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤掠河,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后猛计,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體唠摹,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年奉瘤,在試婚紗的時候發(fā)現(xiàn)自己被綠了勾拉。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡盗温,死狀恐怖望艺,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情肌访,我是刑警寧澤找默,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站吼驶,受9級特大地震影響惩激,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蟹演,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一风钻、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧酒请,春花似錦骡技、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至昼窗,卻和暖如春是趴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背澄惊。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工唆途, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人掸驱。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓肛搬,卻偏偏與公主長得像,于是被迫代替她去往敵國和親毕贼。 傳聞我的和親對象是個殘疾皇子温赔,可洞房花燭夜當晚...
    茶點故事閱讀 45,675評論 2 359