专栏名称: Python开发者
人生苦短,我用 Python。伯乐在线旗下账号「Python开发者」分享 Python 相关的技术文章、工具资源、精选课程、热点资讯等。
目录
相关文章推荐
Python开发者  ·  Python3.8即将结束生命周期 ·  2 天前  
Python爱好者社区  ·  强烈建议尽快搞个软考证!(重大利好) ·  3 天前  
Python爱好者社区  ·  吴恩达,yyds ·  4 天前  
51好读  ›  专栏  ›  Python开发者

Python 源码阅读:内存管理机制(1)

Python开发者  · 公众号  · Python  · 2017-10-14 20:10

正文

(点击上方蓝字,快速关注我们)


作者: wklken

如有好文章投稿,请点击 → 这里了解详情


Python的内存管理架构


基本分层


在Objects/obmalloc.c源码中, 给了一个分层划分



可以看到


layer 3: Object-specific memory(int/dict/list/string....)

         Python 实现并维护

         更高抽象层次的内存管理策略, 主要是各类特定对象的缓冲池机制. 具体见前面几篇涉及的内存分配机制

 

layer 2: Python's object allocator

         Python 实现并维护

         实现了创建/销毁Python对象的接口(PyObject_New/Del), 涉及对象参数/引用计数等

 

layer 1: Python's raw memory allocator (PyMem_ API)

         Python 实现并维护, 包装了第0层的内存管理接口, 提供统一的raw memory管理接口

         封装的原因: 不同操作系统 C 行为不一定一致, 保证可移植性, 相同语义相同行为

 

layer 0: Underlying general-purpose allocator (ex: C library malloc)

         操作系统提供的内存管理接口, 由操作系统实现并管理, Python不能干涉这一层的行为


第三层layer 3前面已经介绍过了, 几乎每种常用的数据类型都伴有一套缓冲池机制.


在这里, 我们关注的是layer 2/1


简要介绍下layer 1, 然后重点关注layer 2, 这才是重点


layer 1: PyMem_ API


PyMem_ API是对操作系统内存管理接口进行的封装


查看pymem.h可以看到


// Raw memory interface

// 这里存在三个宏定义, 宏可以避免一次函数调用的开销, 提高运行效率

// 不允许非配空间大小为0的内存空间

#define PyMem_MALLOC(n)     ((size_t)(n) > (size_t)PY_SSIZE_T_MAX ? NULL

                : malloc((n) ? (n) : 1))

 

#define PyMem_REALLOC(p, n) ((size_t)(n) > (size_t)PY_SSIZE_T_MAX  ? NULL

                : realloc((p), (n) ? (n) : 1))

#define PyMem_FREE      free

 

// 这里做了三个函数的声明, 平台独立的 malloc/realloc/free

PyAPI_FUNC(void *) PyMem_Malloc(size_t);

PyAPI_FUNC(void *) PyMem_Realloc(void *, size_t);

PyAPI_FUNC(void) PyMem_Free(void *);

 

// ============================================================

 

// Type-oriented memory interface

// 这里还有三个类型相关的内存接口, 批量分配/重分配 n 个 类型为 type内存

#define PyMem_New(type, n)

  ( ((size_t)(n) > PY_SSIZE_T_MAX / sizeof(type)) ? NULL :  

    ( (type *) PyMem_Malloc((n) * sizeof(type)) ) )

#define PyMem_NEW(type, n)

  ( ((size_t)(n) > PY_SSIZE_T_MAX / sizeof(type)) ? NULL :  

    ( (type *) PyMem_MALLOC((n) * sizeof(type)) ) )

 

#define PyMem_Resize(p, type, n)

  ( (p) = ((size_t)(n) > PY_SSIZE_T_MAX / sizeof(type)) ? NULL :    

    (type *) PyMem_Realloc((p), (n) * sizeof(type)) )

#define PyMem_RESIZE(p, type, n)

  ( (p) = ((size_t)(n) > PY_SSIZE_T_MAX / sizeof(type)) ? NULL :    

    (type *) PyMem_REALLOC((p), (n) * sizeof(type)) )


然后object.c中, 我们关注实现, 三个实现的函数调用了对应的宏


// 使用 C 写Python扩展模块时使用函数而不是对应的宏

void *

PyMem_Malloc(size_t nbytes)

{

    return PyMem_MALLOC(nbytes);

}

 

void *

PyMem_Realloc(void *p, size_t nbytes)

{

    return PyMem_REALLOC(p, nbytes);

}

 

void

PyMem_Free(void *p)

{

    PyMem_FREE(p);

}


这些接口都相对简单


好了, 结束, 开始关注layer 2: Python's object allocator


Python 的内存分配策略


先来看Objects/obmalloc.c中的一段注释


/*

* "Memory management is where the rubber meets the road -- if we do the wrong

* thing at any level, the results will not be good. And if we don't make the

* levels work well together, we are in serious trouble." (1)

*

* (1) Paul R. Wilson, Mark S. Johnstone, Michael Neely, and David Boles,

*    "Dynamic Storage Allocation: A Survey and Critical Review",

*    in Proc. 1995 Int'l. Workshop on Memory Management, September 1995.

*/


Python引入了内存池机制, 用于管理对小块内存的申请和释放


逻辑


1. 如果要分配的内存空间大于 SMALL_REQUEST_THRESHOLD bytes(512 bytes), 将直接使用layer 1的内存分配接口进行分配

2. 否则, 使用不同的block来满足分配需求


整个小块内存池可以视为一个层次结构


1. 内存池(概念上的, 标识Python对于整个小块内存分配和释放的内存管理机制)

2. arena

3. pool

4. block


block


Python内存的最小单位, 所有block长度都是8字节对齐的


注意这里block只是一个概念, 在源代码中并没有实体存在.


不同类型block, 对应不同内存大小, 这个内存大小的值被称为size class.


不同长度的block


* Request in bytes     Size of allocated block      Size class idx

* ----------------------------------------------------------------

*        1-8                     8                       0

*        9-16                   16                       1

*       17-24                   24                       2

*       25-32                   32                       3

*       33-40                   40                       4

*       41-48                   48                       5

*       49-56                   56                       6

*       57-64                   64                       7

*       65-72                   72                       8

*        ...                   ...                     ...

*      497-504                 504                      62

*      505-512                 512                      63

*

*      0, SMALL_REQUEST_THRESHOLD + 1 and up: routed to the underlying

*      allocator.

*/


例如


申请一块大小28字节的内存, 实际从内存中划到32字节的一个block (从size class index为3的pool里面划出)


图示:


注意: 这里有个Size class idx, 这个主要为了后面pool中用到


size class和size class index之间的转换


#define ALIGNMENT               8               /* must be 2^N */

#define ALIGNMENT_SHIFT         3

#define ALIGNMENT_MASK          (ALIGNMENT - 1)

 

// size class index => size class

#define INDEX2SIZE(I) (((uint)(I) + 1)  size class index

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

 

/* 即

    (8 - 1) / 8 = 0

    (16 - 8) / 8 = 1

*/


pool


pool管理block, 一个pool管理着一堆有固定大小的内存块


本质: pool管理着一大块内存, 它有一定的策略, 将这块大的内存划分为多个大小一致的小块内存.


pool size


在Python中, 一个pool的大小通常为一个系统内存页. 4kB


obmalloc.c

 

#define SYSTEM_PAGE_SIZE        (4 * 1024)

#define SYSTEM_PAGE_SIZE_MASK   (SYSTEM_PAGE_SIZE - 1)

 

#define POOL_SIZE               SYSTEM_PAGE_SIZE        /* must be 2^N */

#define POOL_SIZE_MASK          SYSTEM_PAGE_SIZE_MASK


pool组成


pool的4kB内存 = pool_header + block集合(N多大小一样的block)


pool_header


/* 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        */ - size class index

    uint nextoffset;                    /* bytes to virgin block         */

    uint maxnextoffset;                 /* largest valid nextoffset      */

};


pool_header的作用


1. 与其他pool链接, 组成双向链表

2. 维护pool中可用的block, 单链表

3. 保存 szidx , 这个和该poolblock的大小有关系, (block size=8, szidx=0), (block size=16, szidx=1)...用于内存分配时匹配到拥有对应大小blockpool

4. arenaindex, 后面说


结构图:


pool初始化


从内存中初始化一个全新的空的pool


Objects/obmalloc.c的


void *

PyObject_Malloc(size_t nbytes)

{

  ...

 

          init_pool:

            // 1. 连接到 used_pools 双向链表, 作为表头

            // 注意, 这里 usedpools[0] 保存着 block size = 8 的所有used_pools的表头

            /* Frontlink to used pools. */

            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;

                pool->freeblock = *(block **)bp;

                UNLOCK();

                return (void *)bp;

            }

 

            /*

             * Initialize the pool header, set up the free list to

             * contain just the second block, and return the first

             * block.

             */

            // 开始初始化pool_header

            // 这里 size = (uint)(nbytes - 1) >> ALIGNMENT_SHIFT;  其实是Size class idx, 即szidx

            pool->szidx = size;

 

            // 计算获得每个block的size

            size = INDEX2SIZE(size);

 

            // 注意 #define POOL_OVERHEAD           ROUNDUP(sizeof(struct pool_header))

            // bp => 初始化为pool + pool_header size,  跳过pool_header的内存

            bp = (block *)pool + POOL_OVERHEAD;

 

            // 计算偏移量, 这里的偏移量是绝对值

            // #define POOL_SIZE               SYSTEM_PAGE_SIZE        /* must be 2^N */

            // POOL_SIZE = 4kb, POOL_OVERHEAD = pool_header size

            // 下一个偏移位置: pool_header size + 2 * size

            pool->nextoffset = POOL_OVERHEAD + (size maxnextoffset = POOL_SIZE - size;

 

            // freeblock指向 bp + size = pool_header size + size

            pool->freeblock = bp + size;

 

            // 赋值NULL

            *(block **)(pool->freeblock) = NULL;

            UNLOCK();

            return (void *)bp;

        }


初始化后的图



pool进行block分配 – 0 总体代码


总体分配的代码如下


     if (pool != pool->nextpool) {   //

            /*

             * There is a used pool for this size class.

             * Pick up the head block of its free list.

             */

            ++pool->ref.count;

            bp = pool->freeblock; // 指针指向空闲block起始位置

            assert(bp != NULL);

 

            // 代码-1

            // 调整 pool->freeblock (假设A节点)指向链表下一个, 即bp首字节指向的下一个节点(假设B节点) , 如果此时!= NULL

            // 表示 A节点可用, 直接返回

            if ((pool->freeblock = *(block **)bp) != NULL) {

                UNLOCK();

                return (void *)bp;

            }

 

            // 代码-2

            /*

             * Reached the end of the free list, try to extend it.

             */

            // 有足够的空间, 分配一个, pool->freeblock 指向后移

            if (pool->nextoffset maxnextoffset) {

                /* There is room for another block. */

                // 变更位置信息

                pool->freeblock = (block*)pool +

                                  pool->nextoffset;

                pool->nextoffset += INDEX2SIZE(size);

 

                *(block **)(pool->freeblock) = NULL; // 注意, 指向NULL

                UNLOCK();

 

                // 返回bp

                return (void *)bp;

            }

 

            // 代码-3

            /* Pool is full, unlink from used pools. */  // 满了, 需要从下一个pool获取

            next = pool->nextpool;

            pool = pool->prevpool;

            next->prevpool = pool;

            pool->nextpool = next;

            UNLOCK();

            return (void *)bp;

        }


pool进行block分配 – 1 刚开始


内存块尚未分配完, 且此时不存在回收的block, 全新进来的时候, 分配第一块block


(pool->freeblock = *(block **)bp) == NULL


所以进入的逻辑是代码-2


            bp = pool->freeblock; // 指针指向空闲block起始位置

 

            .....

 

            // 代码-2

            /*

             * Reached the end of the free list, try to extend it.

             */

            // 有足够的空间, 分配一个, pool->freeblock 指向后移

            if (pool->nextoffset maxnextoffset) {

                /* There is room for another block. */

                // 变更位置信息

                pool->freeblock = (block*)pool +

                                  pool->nextoffset;

                pool->nextoffset += INDEX2SIZE(size);

 

                *(block **)(pool->freeblock) = NULL; // 注意, 指向NULL

                UNLOCK();

 

                // 返回bp

                return (void *)bp;

            }


结果图示


pool进行block分配 – 2 回收了某几个block


回收涉及的代码


void

PyObject_Free(void *p)

{

    poolp pool;

    block *lastfree;

    poolp next, prev;

    uint size;

 

    pool = POOL_ADDR(p);

    if (Py_ADDRESS_IN_RANGE(p, pool)) {

        /* We allocated this address. */

        LOCK();

        /* Link p to the start of the pool's freeblock list.  Since

         * the pool had at least the p block outstanding, the pool

         * wasn't empty (so it's already in a usedpools[] list, or

         * was full and is in no list -- it's not in the freeblocks

         * list in any case).

         */

        assert(pool->ref.count > 0);            /* else it was empty */

        // p被释放, p的第一个字节值被设置为当前freeblock的值

        *(block **)p = lastfree = pool->freeblock;

        // freeblock被更新为指向p的首地址

        pool->freeblock = (block *)p;

 

        // 相当于往list中头插入了一个节点

 

     ...

    }

}


没释放一个block, 该block就会变成 pool->freeblock 的头节点, 而单链表一个节点如何指向下一个节点呢? 通过赋值, 节点内存空间保存着下个节点的地址, 最后一个节点指向NULL(知道上面代码-1的判断条件了吧>_


假设已经连续分配了5块, 第1块和第4块被释放


此时内存图示


此时再一个block分配调用进来, 执行分配, 进入的逻辑是代码-1


            bp = pool->freeblock; // 指针指向空闲block起始位置

            // 代码-1

            // 调整 pool->freeblock (假设A节点)指向链表下一个, 即bp首字节指向的下一个节点(假设B节点) , 如果此时!= NULL

            // 表示 A节点可用, 直接返回

            if ((pool->freeblock = *(block **)bp) != NULL) {

                UNLOCK();

                return (void *)bp;

            }


pool进行block分配 – 3 pool用完了


pool中内存空间都用完了, 进入代码-3


            bp = pool->freeblock; // 指针指向空闲block起始位置

 

            // 代码-3

            /* Pool is full, unlink from used pools. */  // 满了, 需要从下一个pool获取

            next = pool->nextpool;

            pool = pool->prevpool;

            next->prevpool = pool;

            pool->nextpool = next;

            UNLOCK();

            return (void *)bp;


获取下一个pool(链表上每个pool的block size都是一致的)


好了, pool到此位置, 下篇讲 arena



本系列




看完本文有收获?请转发分享给更多人

关注「Python开发者」,提升Python技能