哈哈哈,看的有点儿兴奋了,笔记下:
在Python的内部同时维护着宏和内存管理函数两种内存管理机制,宏定义可以节省一次函数调用的开销,
提高运行效率,但同时,使用宏是危险的,因为随着Python的演进,内存管理机制的实现可能会发生改变,因为宏的名字虽然是不会改变的,但其实现代码可能会发生改变,导致旧的宏编写的C扩展与新版本的Python产生二进制不兼容,这样十分危险,所以,在Python的内部维护了函数和宏两套接口,自己编写扩展时,最好使用函数
1 小块的内存池
在Python运行的过程中,很多时候都是小块内存的申请,申请后又很快释放,也就意味着大量的malloc和free
操作,导致操作系统频繁的在用户态和核心态之间进行切换,影响py的执行效率,为了解决这个问题Py引入了缓存池的机制(Pymalloc),整个小块内存的内存池可以视为一个层次结构,在这个层次可以分为4层,从上到下依次为:block , pool , arean和内存池
1.1 先看下block吧:
在最底层block是一个确定大小的内存,在py中有很多种block,每个block有不同的内存大小,这个内存的大小称为size class,可以不记得,记得这是最小单元就好,所有block事8字节对齐的,同时,py为block的大小设定了一个上限,当申请的内存大小小于这个上限时,py可以使用不同种类的block来满足需求,大于这个上限的时候,py转交给第一次的内存管理机制来处理,即pyMem函数族,在py2.5版本,这个值为256k
不同种类的block的大小分别为 8,16,32 ..... ,256,每个size class对应一个size class index ,这个index从0开始,假设申请内存为28,会得到一个32的block,能满足的最小值
1.2 那么超过256的内存申请怎么办呢 ? 来一起看下pool吧:
/* 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
};
一组block的集合称为pool,也就是一个pool管理着多个有固定大小的block,一个pool的大小通常是一个内存页,py将其定义为4k,一个pool里边的block的大小是固定的,比如,可能管理了100个32k的block,也可能管理了100个64k的block,但是不可能一个pool里既有32k的block也有64k的block,每一个pool都和一个size联系到一起,更确切的说是和一个size class index联系到一起,这就是pool header里边的sindex的意义所在
一起看下pool是怎么把block组合到一起的:
/*
pool->szidx = size;
size = INDEX2SIZE(size);
bp = (block *)pool + POOL_OVERHEAD;
pool->nextoffset = POOL_OVERHEAD + (size << 1);
pool->maxnextoffset = POOL_SIZE - size;
pool->freeblock = bp + size;
*(block **)(pool->freeblock) = NULL;
UNLOCK();
return (void *)bp;
pool中的第一块内存是分配给pool_header的,所以pool的结构体中ref.count是1,第一块地址被分配后返回bp的是一个实际地址(也就是申请pool之后的返回),在这个地址之后会有进4k的内存都是实际可用的,但是可以肯定的是申请内存的函数只会使用[bp,bp+size]这个区间的内存,这是size class index保证的,
我该怎么画图呢 ? ......先跳过了
/*
++pool->ref.count;
bp = pool->freeblock;
assert(bp != NULL);
if ((pool->freeblock = *(block **)bp) != NULL) {
UNLOCK();
return (void *)bp;
}
/*
if (pool->nextoffset <= pool->maxnextoffset) {
/* There is room for another block. */
pool->freeblock = (block*)pool +
pool->nextoffset += INDEX2SIZE(size);
*(block **)(pool->freeblock) = NULL;
UNLOCK();
return (void *)bp;
}
假设连续申请5块大小为28字节的内存,由于28字节的对应size class index为3,所以会申请5块32字节的内存,原来freeblock执行的是下一个可用bock的起始地址,再次申请是,只需要返回freeblock指向的地址就可
以了,很显然,freeblock需要前进,指向下一个可用block,此时nextoffset就发光发热了,在pool header里,nextoffset和maxoffset是对pool中的block步进迭代的变量,偏移地址恰好是下一个block的可用地址,在分配
完一个block之后,freeblock和nextoffset就移动一个block的距离,maxoffset则定义了最大的可用大小,划定了block的边界,nextoffset > maxoffset的时候,pool中的block就遍历完了,恩,就是这样的,申请->前进->
申请->前进......
但是假如一个block用完释放了,假如申请了连续5块32字节的内存,2用完释放了,下一个申请是用2还是6 ?
一旦py运行会有大量的这种内存碎片产生,py必须用一种机制把离散的内存管理起来,这就是自由的block链表,链表的关键就是header中的freeblock,回到上边,pool申请初始化完成之后,会指向一个有效地址,为下一个可分配的内存地址,还设置了一个*freeblock,等到有内存释放的时候,freeblock就会变成一个小精灵,
在这4k的内存上灵活舞动,哈哈~~
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
assert(pool->ref.count > 0);/* else it was empty */
*(block **)p = lastfree = pool->freeblock;
pool->freeblock = (block *)p;
if (lastfree) {
struct arena_object* ao;
uint nf; /* ao->nfreepools */
/* freeblock wasn't NULL, so the pool wasn't full,
if (--pool->ref.count != 0) {
/* pool isn't empty: leave it in usedpools */
UNLOCK();
return;
}
来看下,block释放的时候,freeblock指向一个有效的pool地址,但是*freeblock是NULL,假设释放的block是blockA,在pool->freeblock = (block *)p;的时候,freeblock的值被更新了,指向了blockA的首地址,在释放下一个block的时候,同样通过
*(block **)p = lastfree = pool->freeblock;
pool->freeblock = (block *)p;
把释放的block连接到一起,形成自由链表,在需要内存的时候可以很方便的遍历这个链表,优先使用被释放的内存,当pool->freeblock为NULL的时候就不存在离散的自由链表了,分配nextblock即可,nextoffset > maxoffset时,一个pool就用完了,怎么办 ?哈哈,莫着急,一定是存在一个pool的集合的,这就是下边要说的arean