title: "《Python源码剖析》第三部分——Python虚拟机进阶" date: 2017-07-14T00:00:00 categories: [Python] tags: [python]
进程启动后创建 PyInterpreterObject,PyInterpreterObject 里面维护了全局 module 映射表interp->modules
,该表默认初始化为buildin模块,
Python 虚拟机在执行“import A”时,会为 package A 创建一个 module 对象,同时会在该 module 维护的 dict 中添加两个表示元信息的属性:name和path。而 Python 虚拟机从 A/init.py 中执行“import mod1”时,也会为 mod1 创建一个 module 对象,同时也会设置name属性,但是这时就不设置path属性了。
package 是由 module 聚合而成。更清楚的表述是:module 属于一个 package。我们不能说,module1 属于 module2。我们前面已经看到,module 的路径实际上是一种树状结构,从图 14-11 中可以看到,在这个树状结构中,module 的父节点只能是 package,而不可能是另一个 module。
Python 虚拟机使用一个全局解释器锁(Global Interpreter Lock,GIL)来互斥线程对 python 虚拟机的使用。
注意这里 GIL 是解释器一级的互斥锁,也就是同一时间只能有一个线程占用 python 解释器。所以GIL 是用来让操作系统中分配的多个线程互斥的使用 python 解释器的,是建立在系统线程调度基础之上的一套 C API 互斥机制,是比操作系统线程资源更大粒度的锁。
Python 的线程是基于操作系统原生线程的,所以 python 的线程不是「虚拟出来的」。
那么究竟 Python 会在众多的等待线程中选择哪一个幸运儿呢?答案是,不知道。没错,对于这个问题,Python 完全没有插手,而是交给了底层的操作系统来解决。也就是说,Python 借用了底层操作系统所提供的线程调度机制来决定下一个进入 Python 解释器的线程究竟是谁。
GIL 在 C 里对应的结构:
[thread_nt.h]
typedef struct NRMUTEX {
LONG owned ;
DWORD thread_id ;
HANDLE hevent ;
} NRMUTEX, *PNRMUTEX ;
其中owned
初始化为-1,表示锁可用,否则为不可用。thread_id
代表线程 id,最后一个是平台相关的变量,win32 上是一个 event 内核对象。
当 Python 启动时,是并不支持多线程的。换句话说,Python 中支持多线程的数据结构以及 GIL 都是没有创建的,Python 之所以有这种行为是因为大多数的 Python 程序都不需要多线程的支持
书中指出,由于 python 的多线程标准调度机制是有代价的,所以默认单线程不初始化 GIL。
ident = PyThread_start_new_thread(t_bootstrap, (void*) boot);
函数调用操作系统内核接口创建子线程,然后主线程挂起等待obj.done
。注意,此时主线程中持有 GIL。obj.done
,唤醒等待中的主线程。此刻,主线程和子线程都同时由操作系统调度,但是主线程一直持有着 GIL。_Py_Ticker
结束才将自己挂起,让出 GIL(_Py_Ticker
会在每次执行一条字节码后自动减 1,初始默认为 100)。通过上面 4 步,python 的两个线程就完成了从系统调度上升到 python 标准 GIL 调度的流程。
如同上面流程介绍的,标准调度是 python 使用软件时钟调度线程,那么有时候 python 的线程会自我阻塞,比如raw_input()
、sleep()
等函数,这时 python 就会使用阻塞调度的方式。
sleep(1)
后,调用Py_BEGIN_ALLOW_THREADS
立刻释放 GIL,然后调用操作系统的 sleep 操作。此时主线程就由操作系统自动管理。Py_END_ALLOW_THREADS
再次申请 GIL,重新进入 python 标准调度流程。可见 python 在保证线程安全的前提下,允许线程在某些时刻脱离 GIL 标准调度流程。
其中Py_BEGIN_ALLOW_THREADS
和Py_END_ALLOW_THREADS
两个负责释放和等待 GIL 的宏的实现如下。
[ceval.h]
#define Py_BEGIN_ALLOW_THREADS { \
PyThreadState *_save; \
_save = PyEval_SaveThread();
#define Py_END_ALLOW_THREADS PyEval_RestoreThread(_save); \
}
[ceval.c]
PyThreadState* PyEval_SaveThread(void)
{
PyThreadState *tstate = PyThreadState_Swap(NULL);
if (interpreter_lock)
PyThread_release_lock(interpreter_lock);
return tstate;
}
void PyEval_RestoreThread(PyThreadState *tstate)
{
if (interpreter_lock) {
int err = errno;
PyThread_acquire_lock(interpreter_lock, 1);
errno = err;
}
PyThreadState_Swap(tstate);
}
用户级的互斥锁利用操作系统的互斥机制实现,同时要考虑防止和 GIL 形成死锁。所以过程与阻塞调度类似需要使用Py_BEGIN_ALLOW_THREADS
和Py_END_ALLOW_THREADS
这两个宏。
Py_BEGIN_ALLOW_THREADS
释放 GIL 防止死锁。Py_END_ALLOW_THREADS
等待 GIL。在线程的全部计算完成之后,Python 将销毁线程。需要注意的是,Python 主线程的销毁与子线程的销毁是不同的,因为主线程的销毁动作必须要销毁 Python 的运行时环境,而子线程的销毁则不需要进行这些动作。
大块内存管理直接调用 C 的 malloc 和 free 接口,小块内存分配则由 python 的内存池管理机制调度。
Python 的内存块叫 block,每个 block 大小不同,都是 8 的整数倍。管理 block 的叫 pool,一个 pool 是 4K。pool 管理相同大小的一堆 block。pool 对象的 szindex 变量保存了这个 pool 对应的 block 大小。
,一个 pool 可能管理了 100 个 32 个字节的 block,也可能管理了 100 个 64 个字节的 block,但是绝不会有一个管理了 50 个 32 字节的 block 和 50 个 64 字节的 block 的 pool 存在
Python 对于内存块的管理类似对象的策略,每次内存分配一整个 block,回收时先将不用的 Block 加入闲置的队列里等待重新利用,不是直接回收。(惰性回收策略)
管理多个 pool 的数据对象是 arena。下图可见,pool 结构是一次性分配好一块内存,而 arena 则是通过指针连向一块 pool。
而 python 维护一个名叫 arenas 的数组,数组元素就是 arena 对象。arena 之间通过由两条链表相连。它们分别是:
当一个 arena 的 area_object 没有与 pool 集合建立联系时,这时的 arena 处于“未使用”状态;一旦建立了联系,这时 arena 就转换到了“可用”状态。对于每一种状态,都有一个 arena 的链表。“未使用”的 arena 的链表表头是 unused_arena_objects、arena 与 arena 之间通过 nextarena 连接,是一个单向链表;而“可用”的 arena 的链表表头是 usable_arenas、arena 与 arena 之间通过 nextarena 和 prevarena 连接,是一个双向链表。
Pool 是 python 管理内存的对象,arena 虽然更上层,但是 arena 内的 pool 集合可能管理 32 字节的 block,也可能管理 64 字节的 block,所以 arena 无法决定销毁和分配内存。Python 仍然以 pool 为单位管理内存开销。(pool 有 size 概念,arena 没有 size 概念)
Pool 有三种状态 full、empty 和 used。其中 full 不需要连接起来,其他两种状态会被 freepools 和 usedpools 连接起来方便管理。
arena 可以指向 32 位 pool 集合,也可以指向 64 位 pool 集合。分配内存的过程如下:
当 Python 在 WITH_MEMORY_LIMITS 编译符号打开的背景下进行编译时,Python 内部的另一个符号会被激活,这个名为 SMALL_MEMORY_LIMIT 的符号限制了整个内存池的大小,同时,也就限制了可以创建的 arena 的个数。在默认情况下,不论是 Win32 平台,还是 unix 平台,这个编译符号都是没有打开的,所以通常 Python 都没有对小块内存的内存池的大小做任何的限制。
(此部分摘自书中代码注释)
在 2.5 之前版本,Python 的 arena 从来不释放 pool。这就造成反复分配小内存后造成的 arena 太多而内存无法回收。
2.5 之后的处理办法:arena 有两种状态,unused 和 usable。上文已经介绍过。
除了计数器,python 还是使用了标记-清除,分代回收机制。
根据系统内所有对象的引用情况建立有向图,沿着有向图从根开始的逐层染色,黑色代表该节点所有引用都检查过了,灰色表示节点是可达的,当所有灰色节点都变为黑色,检查结束。
Python 的对象由三大部分组成,PyGC_Head,PyObject_Head 和本体。其中 PyObject_Head 里存计数器用来标记当前节点是否可回收,但是对于循环引用的情况,就需要 PyGC_Head 里的 refs,python 会根据一些触发条件进行三色模型的标记,某个对象的「可达次数」标记在 PyGC_Head 里,当这个可达次数为 0 时,代表对象不可达,也就需要回收之。PyGC_Head 之间有一条双向链表连接了所有对象,将他们纳入内存回收管理系统里。
PyGC_Head.gc.gc_ref
还不为 0,这就意味着存在对这些对象的外部引用,这些对象,就是开始标记 - 清除算法的 root object 集合。这种以空间换时间的总体思想是:将系统中的所有内存块根据其存活时间划分为不同的集合,每一个集合就称为一个“代”,垃圾收集的频率随着“代”的存活时间的增大而减小,也就是说,活得越长的对象,就越可能不是垃圾,就应该越少去收集。
Python 采用了三代的分代收集机制,如果当前收集的是第 1 代,那么在开始垃圾收集之前,Python 会将比其“年轻”的所有代的内存链表(当然,在这里只有第 0 代)整个地链接到第 1 代内存链表之后,这个操作是通过 gc_list_merge 实现的。
__del__
函数,则不能安全回收,需要将这些对象收集到 finalizers 链表中,因此,这些对象引用的对象也不能回收,也需要放入 finalizers 链表中__del__
操作的实例对象收集到 Python 内部维护的名为 garbage 的链表中,同时将 finalizers 链表中所有对象加入 old 链表中注意,如果对象拥有__del__
方法,就不能通过垃圾回收来自动回收,所以要慎重使用这个方法。