码迷,mamicode.com
首页 > 编程语言 > 详细

《Python 源码剖析》一些理解以及勘误笔记(3)

时间:2015-05-09 13:28:39      阅读:174      评论:0      收藏:0      [点我收藏+]

标签:python源码   勘误理解笔记   

以下是本人阅读此书时理解的一些笔记,包含一些影响文义的笔误修正,当然不一定正确,贴出来一起讨论。

注:此书剖析的源码是2.5版本,在python.org 可以找到源码。纸质书阅读,pdf 贴图。

文章篇幅太长,故切分成3部分,这是第三部分。


p316: 初始化线程环境

Python 虚拟机运行期间某个时刻整个的运行环境如下图:

技术分享


建立联系之后的PyThreadState 对象和 PyInterpreterState  对象的关系如下图:

技术分享


_PyThreadState_Current 是个全局变量,是当前活动线程对应的 PyThreadState 对象;

interp->modules 指向一个 PyDictObject 对象(module_name, module_object),维护系统所有的module,可能动态添加,为所有PyThreadState 对象所共享;import sys  sys.modules  or sys.__dict__[‘modules‘] 可以访问到module 集合。同理 interp->builtins 指向 __builtins__.__dict__;  interp->sysdict 指向 sys.__dict__; 


p320: 系统module 的初始化

在 Python 中,module 通过 PyModuleObject 对象来实现。

 C++ Code 
1
2
3
4
typedef struct {
    PyObject_HEAD
    PyObject *md_dict;
} PyModuleObject;

在初始化  __builtin__ 模块时,需要将Python 的内置类型对象塞到 md_dict 中,此外内置函数也需要添加。

如  __builtins__.__dict__[‘int‘] 显示为 <type ‘int‘>; __builtins__.__dict__[‘dir] 显示为<built-in function dir>;

系统的 __builtin__ 模块的 name为  ‘__builtin__ ‘, 即 __builtins__.__dict__[‘__name__‘] 显示为 ‘__builtin__ ‘;

__builtins__.__dict__[‘__doc__‘] 显示为 "Built-in functions, exceptions, .... ";

也可直接 __builtins__.__name__ ,   __builtins__.__doc__;


这里解释下为什么会出现 ‘__builtins__‘。我们经常做单元测试使用的机制 if __name__ == ‘__main__‘ ,表明作为主程序运行的Python 源文件可以被视为名为 __main__ 的 module,当然以 import py 方式加载,则__name__ 不会为 __main__。在初始化 __main__ module 时会将(‘__builtins__‘, __builtin__ module)插入到其 dict 中。也就是说‘__builtins__‘ 是 dict 中的一个 key。比如在命令行下输入 dir() ,输出为 [‘__builtins__‘, ‘__doc__‘, ‘__name__ ‘]。实际上在激活字节码虚拟机的过程中创建的第一个PyFrameObject 对象,所设置的 local、global、builtin 名字空间都是从__main__ 模块的dict 中得到的,当然在真正开始运行指令时,local or global 会动态增删。


builtin_methods 中每一个函数对应一个 PyMethodDef 结构,会基于它创建一个 PyCFunctionObject 对象,这个对象是Python 对函数指针的包装。

 C++ Code 
1
2
3
4
5
6
7
struct PyMethodDef {
    const char  *ml_name;   /* The name of the built-in function/method */
    PyCFunction  ml_meth;   /* The C function that implements it */
    int      ml_flags;  /* Combination of METH_xxx flags, which mostly
                   describe the args expected by the C func */

    const char  *ml_doc;    /* The __doc__ attribute, or NULL */
};

 C++ Code 
1
2
3
4
5
6
typedef struct {
    PyObject_HEAD
    PyMethodDef *m_ml; /* Description of the C function to call */
    PyObject    *m_self; /* Passed as ‘self‘ arg to the C func, can be NULL */
    PyObject    *m_module; /* The __module__ attribute, can be anything */
} PyCFunctionObject;

技术分享


__builtin__  module 初始化完成后如下图:

技术分享


在完成了__builtin__ 和 sys 两个模块的设置之后,内存布局如下图:

技术分享

Python 内部维护了一个全部变量extensions,这个PyDictObject 对象将维护所有已经被Python 加载的module 中的

PyDictObject 的一个备份。当Python 系统的 module 集合中的某个标准扩展module 被删除后不久又被重新加载时,Python 就不需要再次初始化这些module,只需要用extensions 中备份的 PyDictObject 对象来创建一个新的module 即可。这一切基于假设:Python 中的标准扩展module 是不会在运行时动态改变的。实际上Python 内部提供的module 可以分成两类,一类是C 实现的builtin module 如thread,一类是用python 实现的标准库module。


p328:设置搜索路径、site-specific 的 module 搜索路径

sys.path 即 sys.__dict__[‘path‘] 是一个 PyListObject 对象,包含了一组PyStringObject 对象,每一个对象是一个module 的搜索路径。

第三方库路径的添加是 lib/site.py 完成的,在site.py 中完成两个动作:

1. 将 site-packages 路径加入到 sys.path 中。

2. 处理 site-packages 目录下所有.pth 文件中保存的所有路径加入到 sys.path。


完成初始化后的环境如下图所示:

技术分享

p347: import 机制的黑盒探测

 Python Code 
1
2
3
# hello.py
a = 1
b = 2
import hello 之后,使用dir() 获得的信息

技术分享

技术分享

也就是说,hello module 中的 __builtins__ 符号对应的 dict 正是当前名字空间中 __builtins__ 符号对应的module对象所维护的那个dict 对象。

注意 from hello import a 的情况有所不同:

技术分享

 注意 from hello import * ,如果 hello.py 定义了__all__ = [ ... ],那么只加载列表里面的符号;当然如果在 __init__.py 定义了 __all__,那么只加载列表中的 module。类似地,import A.tank as Tank 在locals() 中出现的名字是 ‘Tank‘,但还是需要通过 sys.modules[‘A.tank‘] 才能正确访问。


如果出现了嵌套import 的情况呢?

 Python Code 
1
2
3
4
5
# usermodule1.py
import usermodule2

# usermodule2.py
import sys
技术分享


也就是说在每个py 中进行的import 动作并不会影响上一层的名字空间,只是影响各个module 自身的名字空间;但所有import 动作,无论发生在什么时间、什么地点,都会影响到全局module 集合即 sys.modules。图中的 __file__ 是文件路径名。


实际上“动态加载”真实含义是将这个module 以某个符号的形式引入到某个名字空间,del xxx 只是删除了符号,而sys.modules 中仍然维护了xxx 对应的module 对象。如果我们更新了module A 的某个功能实现,可以使用 reload 来更新 sys.modules 中维护的module A 对象,注意:Python 虚拟机在调用reload() 操作更新module 时,只是将新的符号加入到module 中,而不管module 中的先前符号是否已经在源文件中被删除了。


只有在文件夹中有一个特殊的 __init__.py 文件,Python 虚拟机才会认为这是一个合法的package,当Python 虚拟机执行 import A 时,会动态加载目录A 的 __init__.py,除非 __init__.py 中有显式的import 语句,否则只会将package 自身加载到Python,并不会对package 中的module 进行任何动作。

在加载package 下的module 时,比如 A.B.c,Python 内部将这个module 的表示视为一个树状的结构。如果 A 在import A.D 时被加载了,那么在 A 对应的PyModuleObject 对象中的dict 中维护着一个 __path__,表示这个 package 的搜索路径,那么接下来对B 的搜索将只在 A.__path__ 中进行,而不在所有Python 搜索路径中执行了(直接 import module 时)。


p362: import 机制的实现

 Python Code 
1
2
3
4
5
6
7
import sys
import xml.sax # xml/sax.py
from xml import sax 
from xml.sax import xmlreader
from sys import path 
from sys import path as mypath
import usermodule

如上举例说明 " from A import mod",尽管 mod 并不在 A 对应的module 对象的名字空间中,但是import 机制能够根据 A 发现 mod,也是合法的。

Python import 机制的起点是builtin module 的 __import__ 操作,也就是 builtin__import__ 函数。

Python 将寻找一切可以作为module的文件,比如subname 来说,Python 将寻找 subname.py、subname.pyc、subname.pyd、subname.pyo、subname.dll(Python 2.5 已不再执行dll 后缀名的文件)。

对于py 文件,Python 虚拟机会先对py 文件进行编译产生PyCodeObject 对象,然后执行了co_code 字节码,即通过执行def、class 等语句创建PyFunctionObject、PyClassObject 等对象,最后得到一个从符号映射到对象的dict,自然也就是所创建的module 对象中维护的那个dict。

import 创建的module 都会被放到全局module 集合 sys.module 中。


p386: 与 module 有关的名字空间问题

 Python Code 
1
2
3
4
5
6
7
8
9
10
11
# module1.py
import module2

owner = "module1"
module2.show_owner() # "module2"


# module2.py
owner = "module2"
def show_owner():
    print owner 

在执行 import module2 时,Python 会创建一个新的PyFrameObject 对象,刚开始这个对象中的global 名字空间可能只有 ‘__builtins__‘ 

、‘__doc__‘、‘__name__‘ 等符号,随着代码的执行,global名字空间会增加 ‘owner‘ 和‘show_owner‘,并且‘show_owner‘ 对应的PyFunctionObject 对

象中的 func_globals 保存了当前 global名字空间的所有符号。在 module1.py 中执行完 owner = "module1" 后, module1的global 名字空间大概是 

{..., ‘owner‘ : ‘module1‘, ‘module2‘: <module ‘module2...‘>},在执行 module2.show_owner() 时,首先要获得符号‘show_owner‘ :

技术分享

执行函数需要创建一个PyFrameObject 对象,根据笔记(1)的条目p226所说,PyFrame_New(tstate, co, globals, NULL) 中的

global 参数是来自 PyFunctionObject.func_globals,故根据LEGB原则,print owner 输出的是‘module2‘ 。


p392: Python 的线程在GIL(global interpreter lock) 的控制之下,线程之间,对整个Python 解释器(虚拟机),对Python 提供的 C API 的访问,都是互斥的,这可以看作是 Python 内核级的互斥机制。Python 内部维护一个数值N(sys.getcheckinterval() ),模拟内部的“软件中断”,当执行了某个线程的N 条指令之后应该立刻启动线程调度机制,因为 Python 中的线程实际上就是操作系统所支持的原生线程,故下一个被调度运行的线程由操作系统来选择。


p394: Python 线程的创建

threadmodule.c 提供的函数接口很少,定义在 static PyMethodDef thread_methods[] = { ...};   

Python 虚拟机通过三个主要的动作,完成一个线程的创建:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//threadmodule.c
// thread.start_new_thread 对应的 c 实现函数
static PyObject *
thread_PyThread_start_new_thread(PyObject *self, PyObject *fargs)
{
    ...
    boot = PyMem_NEW(struct bootstate, 1); // 1)
    ...
    PyEval_InitThreads(); /* Start the interpreter‘s thread-awareness */ // 2)
    ...
    ident = PyThread_start_new_thread(t_bootstrap, (void *) boot); // 3)
    ...
    return indent;
}

1). 创建并初始化bootstate 结构,保存线程的函数、函数参数等。

 C++ Code 
1
2
3
4
5
6
7
struct bootstate
{
    PyInterpreterState *interp;
    PyObject *func;
    PyObject *args;
    PyObject *keyw;
};
其中 boot->interp 指向了PyInterpreterState 对象,这个对象携带了如module pool 这样的全局信息,被所有 thread 所共享。


2). 初始化Python 的多线程环境。

当Python 启动时是不支持多线程的(线程的调度需要代价),一旦用户调用 thread.start_new_thread,Python 意识到用户需要多线程的支持,自动创建多线程机制需要的数据结构、环境以及重要的GIL 等。

 C++ Code 
1
2
3
4
5
6
// pythread.h
typedef void *PyThread_type_lock;

// ceval.c
static PyThread_type_lock interpreter_lock = 0/* This is the GIL */
static long main_thread = 0;

如上定义可以看到实际上 GIL 就是一个 void* 指针,无论建立多少个线程,初始化只进行一次。

Python 多线程机制具有平台相关性,在Python/Python 目录下有一批 thread_***.h 的头文件,包装了不同操作系统的原生线程,并通过统一的接口暴露给 Python,比如thread_nt.h 包装的是win32 平台的原生 thread,interpreter_lock 就是指向了如下的 NRMUTEX 结构。

 C++ Code 
1
2
3
4
5
6
7
//thread_nt.h
typedef struct NRMUTEX
{
    LONG   owned ;
    DWORD  thread_id ;
    HANDLE hevent ;
} NRMUTEX, *PNRMUTEX ;
其中 hevent 对应 win32 下的 Event 这个内核对象,以实现线程的互斥;thread_id 记录任一时刻获得 GIL 的线程id;owned 初始化为-1表示可用,当一个线程开始等待 GIL 时owned 值原子性地加1,释放时原子性地减1。当一个线程释放GIL 时会通过 SetEvent 通知等待 Event 内核对象的所有线程,此时调度线程运行的难题就交给了操作系统选择。

当初始化环境完毕之后主线程(执行python.exe 时操作系统创建的线程)首先获得 GIL 控制权。


3). 以bootstate 为参数创建操作系统的原生线程。

在 PyThread_start_new_thread 中首先将t_bootstrap 和 boot  打包到一个类型为 callobj 的结构体obj 中,如下所示:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
long PyThread_start_new_thread(void (*func)(void *), void *arg)
{
    callobj obj;
    obj.done = CreateSemaphore(NULL01NULL);
    ...
    rv = _beginthread(bootstrap, _pythread_stacksize, &obj);
    /* wait for thread to initialize, so we can get its id */
    WaitForSingleObject(obj.done, INFINITE); // 挂起

    return obj.id;
}


 C++ Code 
1
2
3
4
5
6
typedef struct {
    void (*func)(void*);
    void *arg;
    long id;
    HANDLE done;
} callobj;

技术分享


当完成打包之后,调用 win32 下创建thread的api:_beginthread 来完成线程的创建,函数返回后主线程会挂起等待obj.done 这个Semaphore内核对象,子线程开始执行bootstrap ,在其中完成3个动作:1). 获取线程id; 2). 通知obj.done 内核对象; 3). 调用 t_bootstrap;

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
static int bootstrap(void *call)
{
    callobj *obj = (callobj*)call;
    /* copy callobj since other thread might free it before we‘re done */
    void (*func)(void*) = obj->func;
    void *arg = obj->arg;

    obj->id = PyThread_get_thread_ident();
    ReleaseSemaphore(obj->done, 1NULL);
    func(arg);
    return 0;
}
此时正在等待 obj.done 的主线程被唤醒而继续执行,返回了子线程id。而子线程继续执行 t_bootstrap,在里面进入等待 GIL 的状态。

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void t_bootstrap(void *boot_raw)
{
    struct bootstate *boot = (struct bootstate *) boot_raw;
    PyThreadState *tstate;
    PyObject *res;

    tstate = PyThreadState_New(boot->interp);

    PyEval_AcquireThread(tstate); // 申请GIL
    res = PyEval_CallObjectWithKeywords(
        boot->func, boot->args, boot->keyw); // 最终调用 PyEval_EvalFrameEx
    ...
    PyMem_DEL(boot_raw);
    PyThreadState_Clear(tstate);
    PyThreadState_DeleteCurrent(); // 释放 GIL
    PyThread_exit_thread();
}
在子线程得到 GIL 后,PyEval_AcquireThread 返回,PyEval_CallObjectWithKeywords 最终调用 PyEval_EvalFrameEx,在PyEval_EvalFrameEx 中Python 内部维护的模拟时钟中断会不断地激活线程的调度机制,在子线程和主线程之间不断地进行切换,从而真正实现多线程机制。

可以认为到此时子线程的初始化才算真正完成,子线程和主线程一样,都完全被Python 线程调度机制所控制了。需要注意的是:当所有线程都完成了初始化之后,操作系统的线程调度是与Python 的线程调度统一的(Python 线程调度--> GIL --> Event 内核对象 --> 操作系统调度),但在初始化完成之前,它们之间并没有因果关系。


我们知道操作系统在进行进程切换时需要保存or 恢复上下文环境,Python 在进行线程切换时也需要将线程状态保存在 PyThreadState 对象,前面说过,_PyThreadState_Current  这个全局变量一直指向当前被激活的线程对应的 PyThreadState 对象。Python 内部维护了一个单向链表来管理所有Python 线程的状态对象,对于这个链表的访问有一个独立的锁而不必在GIL 保护下进行,此锁的创建在Python 进行初始化时完成。

技术分享

其中 id 是指线程id,如果value 都是指向 PyThreadState 对象,那么它们的 key 值都一致。


p413: Python 线程的调度

Python 的线程调度机制是内建在 Python 的解释器核心 PyEval_EvalFrameEx 中的。除了标准的计数调度外,还存在另一种阻塞调度,即在线程 A通过某种操作比如等待输入或者睡眠等,将自身阻塞后,Python 应该将等待GIL 的线程B 唤醒,当然 A 在挂起前肯定需要释放 GIL。

比如 time.sleep(1) 大概是这样实现的: { Py_BEGIN_ALLOW_THREADS(释放GIL)   sleep(1);   Py_END_ALLOW_THREADS(申请GIL) }

即通过两个宏来实现阻塞调度,注意阻塞调度则不会重置 PyEval_EvalFrameEx 内的 _Py_Ticker 为 初始值 _Py_CheckInterval。


p420: Python 线程的用户级互斥与同步

内核级通过 GIL 实现的互斥保护了内核的共享资源,同样用户级互斥保护了用户程序中的共享资源。

用户级的锁是用 lockobject 实现的,与GIL 一样, lock_lock 也指向一个win32 下的 Event 内核对象。

 C++ Code 
1
2
3
4
typedef struct {
    PyObject_HEAD
    PyThread_type_lock lock_lock;
} lockobject;

lockobject 对象提供的属性操作定义在 static PyMethodDef lock_methods[] = { ... }; 需要注意的是当锁不可用时 lockobject.acquire 操作也是一个阻塞操作,故大概是这样实现的: { Py_BEGIN_ALLOW_THREADS(释放GIL)   PyThread_acquire_lock();   Py_END_ALLOW_THREADS(申请GIL) }

这是由于线程需要等待一个 lock 资源,为了避免死锁,需要将 GIL 转交给 其他的等待 GIL 的线程,然后调用 PyThread_acquire_lock 开始尝试获得

用户级锁,在获得用户级锁之后,再尝试获得内核级lock--GIL。


p430:Python 的内存管理机制

技术分享

layer 0 即操作系统提供的malloc or free 等接口;layer 1 是Python 基于第0 层包装而成,没有加入太多动作,只是为了处理与平台相关的内存分配行为而提供统一的接口; layer 2 主要提供创建Python 对象的接口,这一套函数族又被称为 Pymalloc 机制;layer 3 主要是常用对象如整数、字符串等的缓冲池机制。真正需要分析的是 layer 2 的实现,也是 GC(garbage collector) 的藏身之处。


p432: 小块空间的内存池

在Python 2.5 中,整个小块内存的内存池可以视为一个层次结构,在这个层次结构中,一共分为4层,从上至下为:block、pool、arena 和 内存池。

前三个都是可以在源码中找到的实体,而 “内存池” 只是一个概念上的东西,表示Python 对整个小块内存分配和释放行为的内存管理机制。


在最底层,block 是一个确定大小的内存块(8, 16, 24, ...., 256),大小超过256字节的内存申请转交给layer 1 PyMem 函数族处理。


一个pool 管理一堆具有固定大小的内存块,一个pool 大小通常为一个系统内存页4KB。

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* When you say memory, my mind reasons in terms of (pointers to) blocks */
typedef uchar block;

/* 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  */
};

一块经过改造的 4KB 内存如下图:

技术分享

其中实线箭头是指针,但虚线箭头只是偏移位置的形象表示。

ref.count 表示已经被分配的block 数量,此时为1。bp 返回的是一个可用地址,实际后面的内存都是可用的,但可以肯定申请内存的函数只会使用[bp, bp+size] 区间的内存,比如申请 25~32 字节大小的内存,会返回一个 32字节 block 的地址。

szidx 表示 size class index,比如 szidx=3 表示pool 管理的是 32字节的block 集合。在 pool header 中,nextoffset 和 maxoffset 是两个用于对pool 中的block 集合进行迭代的变量,如初始化时:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
 * Initialize the pool header, set up the free list to
 * contain just the second block, and return the first
 * block.
 */

pool->szidx = size; 
size = INDEX2SIZE(size); // 由szidx 转换成 size
bp = (block *)pool + POOL_OVERHEAD;
pool->nextoffset = POOL_OVERHEAD + (size << 1); // POOL_OVERHEAD + 2*size
pool->maxnextoffset = POOL_SIZE - size;
pool->freeblock = bp + size;
*(block **)(pool->freeblock) = NULL;

return (void *)bp;

freeblock 指向下一个可用 的block 地址,而nextoffset 是下下可用block 距离头部的偏移,故再次分配block 时,只需将freeblock 返回,并将其移动 nextoffset 的距离,同样地 nextoffset 值加上2个size 距离,即如下代码 2)处所示。

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
* There is a used pool for this size class.
* Pick up the head block of its free list.
 */

++pool->ref.count;
bp = pool->freeblock;
assert(bp != NULL);
if ((pool->freeblock = *(block **)bp) != NULL// 1)
{
    return (void *)bp;
}
/*
 * Reached the end of the free list, try to extend it.
 */

if (pool->nextoffset <= pool->maxnextoffset) // 2)
{
    /* There is room for another block. */
    pool->freeblock = (block *)pool +
                      pool->nextoffset;
    pool->nextoffset += INDEX2SIZE(size);
    *(block **)(pool->freeblock) = NULL;
    return (void *)bp;
}

现在考虑一种情况,假设pool 中5个连续的block 都被分配出去了,过一段时间释放了块2 和块4,那么下一次申请32字节内存,pool 返回的是第2块还是第6块呢?出于使用效率显然是第2块,具体实现是在 free block 时做的手脚,将一些离散的资源block 组织起来成为自由block 链表,而freeblock 则是这个链表的表头,如下代码所示:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pool = POOL_ADDR(p); // p 是 PyObject_Free 的参数
if (Py_ADDRESS_IN_RANGE(p, pool))
{
    /* We allocated this address. */

    /* 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 */
    *(block **)p = lastfree = pool->freeblock;
    pool->freeblock = (block *)p;
    ...
 }

技术分享

回到前面分析的block 分配行为,可以知道如果有先前释放的block 则直接返回如1)处代码所示,没有再进行2)处代码判断,在2)处还需指出一点即 maxoffset 是该pool 最后一个可用block 距离pool header 的偏移,故如果

nextoffset > maxnextoffset 则此pool 已经无block 可用了,可以考虑再申请一个pool 了。


一个area 大小是256KB,可以容纳 64个pool,这些pools 可能并不属于同一个 class size index 。pool_header 管理的内存与pool_header 自身是一块连续的内存,而 arena_object 与其管理的内存是分离的,也就是说当arena_object 被申请时,它所管理的pool 集合的内存还没被申请,下面是 arena_object 的定义,每个条目注释都写得非常清楚,我就不狗尾续貂解释了。

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// obmalloc.c
#define ARENA_SIZE      (256 << 10/* 256KB */
/* Record keeping for arenas. */
struct arena_object
{
    /* The address of the arena, as returned by malloc.  Note that 0
     * will never be returned by a successful malloc, and is used
     * here to mark an arena_object that doesn‘t correspond to an
     * allocated arena.
     */

    uptr address;

    /* Pool-aligned pointer to the next pool to be carved off. */
    block *pool_address;

    /* The number of available pools in the arena:  free pools + never-
     * allocated pools.
     */

    uint nfreepools;

    /* The total number of pools in the arena, whether or not available. */
    uint ntotalpools;

    /* Singly-linked list of available pools. */
    struct pool_header *freepools;

    /* Whenever this arena_object is not associated with an allocated
     * arena, the nextarena member is used to link all unassociated
     * arena_objects in the singly-linked `unused_arena_objects` list.
     * The prevarena member is unused in this case.
     *
     * When this arena_object is associated with an allocated arena
     * with at least one available pool, both members are used in the
     * doubly-linked `usable_arenas` list, which is maintained in
     * increasing order of `nfreepools` values.
     *
     * Else this arena_object is associated with an allocated arena
     * all of whose pools are in use.  `nextarena` and `prevarena`
     * are both meaningless in this case.
     */

    struct arena_object *nextarena;
    struct arena_object *prevarena;
};


实际上在 Python 中存在多个 arena_object 构成的数组,数组首地址由全局变量 arenas 维护。当一个 arena_object 没有与pool 集合建立联系(申请pool集合内存)时为“未使用”状态,当建立联系后转为“可用”状态,各自由两个头指针链接起来,如下图:

技术分享

一个pool 在Python 运行的任一时刻,总是处于以下三种状态之一:

  • used 状态:pool 中至少有一个 block 已经被使用,并且至少有一个block 还未被使用。这种状态的pool 受控于 Python 内部维护的 usedpools 数组,对于同样 class size index 的 pools, usedpools 只保存一个引用,而它们之间链接成链表。
  • full 状态:pool 中所有的 block 都已经被使用,这种状态的 pool 在 arena 中,但不在 arena 的 freepools 链表中;
  • empty 状态:pool 中所有block 都未被使用,处于这个状态的pool 的集合通过其pool_header 中的next_pool 构成一个链表,这个链表的表头就是 arena_object 的 freepools;
下图给出了一个“可用”的 arena 包含三种状态的pool 集合的一个可能状态。

技术分享

注意:arena 中处于full 状态的pool 是各自独立的,并没有像其他pool 一样会链接成链表。

接着来看一下存放 pool_head 指针的 usedpools 数组 的定义:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define SMALL_REQUEST_THRESHOLD 256
#define ALIGNMENT       8       /* must be 2^N */
#define NB_SMALL_SIZE_CLASSES   (SMALL_REQUEST_THRESHOLD / ALIGNMENT)

typedef struct pool_header *poolp;
#define PTA(x)  ((poolp )((uchar *)&(usedpools[2*(x)]) - 2*sizeof(block *)))
#define PT(x)   PTA(x), PTA(x)

static poolp usedpools[2 * ((NB_SMALL_SIZE_CLASSES + 7) / 8) * 8] = {
    PT(0), PT(1), PT(2), PT(3), PT(4), PT(5), PT(6), PT(7)
#if NB_SMALL_SIZE_CLASSES > 8
    , PT(8), PT(9), PT(10), PT(11), PT(12), PT(13), PT(14), PT(15)
    ...
#endif
}
即数组初始化完成之后如下图:

技术分享

对比pool_header 结构体,此时我们可以发现这样的规律usedpools[6]->nextpool == usedpools[6], 因为 usedpools[6] 即 usedpools[4] 的地址,向后偏移8个字节(一个ref 加上一个 block*),即 &nextpool,也就是 usedpools[6] 的地址。

当我们手中有一个 size 为 32 字节的pool,想要放入 usedpools 数组,只需要 usedpools[i+i]->nextpool = pool,其中 i 为size class index。在PyObject_Malloc 代码中,利用 usedpools 的巧妙结构,只需要通过简单判断来发现与某个class size index 对应的pool 是否在 usedpools 存在,如下:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
size = (uint)(nbytes - 1) >> ALIGNMENT_SHIFT; // class size index
pool = usedpools[size + size];
if (pool != pool->nextpool) // 简单判断
{  
    /*
     * There is a used pool for this size class.
     * Pick up the head block of its free list.
     */

     // usedpools 有可用的 pool
 }
 ...
 // usedpools 无可用的pool,尝试获取 empty 状态pool

Python 的小块内存的内存池全景可以用下面一幅图展示:

技术分享


p457: 循环引用的垃圾收集

在Python 中,主要的内存管理手段是引用计数机制,而标记--清除(Mark--Sweep)和分代收集只是为了打破循环引用而引入的补充技术。Python 中的循环引用总是发生在 container 对象之间,即是内部可持有对其他对象引用的对象,比如list、dict、class、instance 等。这些container 对象在创建后必须链入到 Python 内部的可收集对象链表中去,故需要在对象头部增加 PyGC_Head,如下图所示:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
/* GC information is stored BEFORE the object structure. */
typedef union _gc_head
{
    struct
    {
        union _gc_head *gc_next;
        union _gc_head *gc_prev;
        Py_ssize_t gc_refs;
    } gc;
    long double dummy;  /* force worst-case alignment */
} PyGC_Head;
技术分享

在 Python 中有一个维护了三个 gc_generation 结构的数组,通过这个数组控制流三条可收集对象链表,这就是Python 中用于分代垃圾收集的三个“代”。初始化完毕如下图所示:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*** Global GC state ***/

struct gc_generation
{
    PyGC_Head head;
    int threshold; /* collection threshold */
    int count; /* count of allocations or collections of younger
              generations */

};

#define NUM_GENERATIONS 3
#define GEN_HEAD(n) (&generations[n].head)

/* linked lists of container objects */
static struct gc_generation generations[NUM_GENERATIONS] =
{
    /* PyGC_Head,               threshold,  count */
    {{{GEN_HEAD(0), GEN_HEAD(0), 0}},   700,        0},
    {{{GEN_HEAD(1), GEN_HEAD(1), 0}},   10,     0},
    {{{GEN_HEAD(2), GEN_HEAD(2), 0}},   10,     0},
};

PyGC_Head *_PyGC_generation0 = GEN_HEAD(0);
技术分享

对于每一个gc_generation,其中的count 记录了当前这条可收集对象链表中一共有多少个可收集对象,所有新创建的对象实际上都会被加入第0代链表中,即 generations[0].count++。而 threshold 记录了该条链表最多可容纳多少个可收集对象,一旦超过将立刻触发垃圾回收机制。


关于回收机制的大致描述:

/* Find the oldest generation (higest numbered) where the count exceeds the threshold.  Objects in the that generation and generations younger than it will be collected. */

具体实现可以看collect 函数,下面列举比较重要的步骤及注释:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91

/* This is the main function.  Read this to understand how the
 * collection process works. */

static Py_ssize_t
collect(int generation)
{
    int i;
    Py_ssize_t m = 0/* # objects collected */
    Py_ssize_t n = 0/* # unreachable objects that couldn‘t be collected */
    PyGC_Head *young; /* the generation we are examining */
    PyGC_Head *old; /* next older generation */
    PyGC_Head unreachable; /* non-problematic unreachable trash */
    PyGC_Head finalizers;  /* objects with, & reachable from, __del__ */
    PyGC_Head *gc;
    double t1 = 0.0;

    if (delstr == NULL) {
        delstr = PyString_InternFromString("__del__");
        if (delstr == NULL)
            Py_FatalError("gc couldn‘t allocate \"__del__\"");
    }

    ...
    /* update collection and allocation counters */
    if (generation+1 < NUM_GENERATIONS)
        generations[generation+1].count += 1;
    for (i = 0; i <= generation; i++)
        generations[i].count = 0;

    /* merge younger generations with one we are currently collecting */ // 1)
    for (i = 0; i < generation; i++) {
        gc_list_merge(GEN_HEAD(i), GEN_HEAD(generation));
    }

    ...
    /* Using ob_refcnt and gc_refs, calculate which objects in the
     * container set are reachable from outside the set (i.e., have a
     * refcount greater than 0 when all the references within the
     * set are taken into account).
     */

    update_refs(young); // 2)
    subtract_refs(young); 

    /* Leave everything reachable from outside young in young, and move
     * everything else (in young) to unreachable.
     * NOTE:  This used to move the reachable objects into a reachable
     * set instead.  But most things usually turn out to be reachable,
     * so it‘s more efficient to move the unreachable things.
     */

    gc_list_init(&unreachable);
    move_unreachable(young, &unreachable); // 3)

    /* Move reachable objects to next generation. */
    if (young != old)
        gc_list_merge(young, old);

    /* All objects in unreachable are trash, but objects reachable from
     * finalizers can‘t safely be deleted.  Python programmers should take
     * care not to create such things.  For Python, finalizers means
     * instance objects with __del__ methods.  Weakrefs with callbacks
     * can also call arbitrary Python code but they will be dealt with by
     * handle_weakrefs().
     */

    gc_list_init(&finalizers);
    move_finalizers(&unreachable, &finalizers);
    /* finalizers contains the unreachable objects with a finalizer;
     * unreachable objects reachable *from* those are also uncollectable,
     * and we move those into the finalizers list too.
     */

    move_finalizer_reachable(&finalizers); // 4)


    /* Clear weakrefs and invoke callbacks as necessary. */
    m += handle_weakrefs(&unreachable, old);

    /* Call tp_clear on objects in the unreachable set.  This will cause
     * the reference cycles to be broken.  It may also cause some objects
     * in finalizers to be freed.
     */

    delete_garbage(&unreachable, old);  // 5)


    /* Append instances in the uncollectable set to a Python
     * reachable list of garbage.  The programmer has to deal with
     * this if they insist on creating this type of structure.
     */

    (void)handle_finalizers(&finalizers, old);

    
    return n+m;
}

下图是用于演示标记--清除算法的例子:

技术分享

1). 将比当前代年轻的一代链接在其后面,此后的标记清除算法在此条链表进行。

2). update_refs 将PyGC_Head 中的gc.gc_ref 赋值为 其对象 ob_refcnt 值;subtract_refs 将循环引用摘除,即对 gc.gc_ref 值做相应减法操作。完成后如下图:

技术分享

3). 将待处理链表的unreachable object 移动到 unreachable 链表中。处理完成后如下图所示:

技术分享

这里说明下reachable的含义,也就是所谓的标记阶段:所谓的root object 是被一些全局引用和函数栈中的引用所引用的对象,这些对象是不可被删除的;从 root object 集合出发,沿着集合中的每一个引用,如果能够达到某个对象 A,则称 A 是可达的 reachable。

4). 对于一类特殊的container 对象,即从类对象实例化得到的实例对象,如果 class 定义中有一个特殊方法:‘__del__‘,则将unreachable 链表中拥有 finalizer 的 instance 对象都移动到一个名为 garbage 的PyListObject 对象中。

5). 在3) 中只是使用 gc.gc_ref 模拟量循环引用的打破,在 delete_garbage 中会调用container 对象的类型对象中的tp_clear 操作,进而调整container 对象中每个引用所用的对象的引用计数值,从而完成打破循环引用的最终目标。


注:可以使用Python 中的gc 模块来观察垃圾回收,如

import gc

gc.set_debug(gc.DEBUG_STATS | gc.DEBUG_LEAK)

gc.collect()



《Python 源码剖析》一些理解以及勘误笔记(3)

标签:python源码   勘误理解笔记   

原文地址:http://blog.csdn.net/jnu_simba/article/details/45479115

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!