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

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

注:此书剖析的源码是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(NULL, 0, 1, NULL);

...

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, 1, NULL);

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()

时间: 2024-10-08 10:55:19

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

《python源码剖析》笔记 Python虚拟机框架

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 1. Python虚拟机会从编译得到的PyCodeObject对象中依次读入每一条字节码指令, 并在当前的上下文环境中执行这条字节码指令. Python虚拟机实际上是在模拟操作中执行文件的过程 PyCodeObject对象中包含了字节码指令以及程序的所有静态信息,但没有包含 程序运行时的动态信息--执行环境(PyFrameObject) 2.Python源码中的PyFrameObject

Python源码剖析笔记3-Python执行原理初探

Python源码剖析笔记3-Python执行原理初探 本文简书地址:http://www.jianshu.com/p/03af86845c95 之前写了几篇源码剖析笔记,然而慢慢觉得没有从一个宏观的角度理解python执行原理的话,从底向上分析未免太容易让人疑惑,不如先从宏观上对python执行原理有了一个基本了解,再慢慢探究细节,这样也许会好很多.这也是最近这么久没有更新了笔记了,一直在看源码剖析书籍和源码,希望能够从一个宏观层面理清python执行原理.人说读书从薄读厚,再从厚读薄方是理解了

Python源码剖析笔记0 ——C语言基础

python源码剖析笔记0--C语言基础回顾 要分析python源码,C语言的基础不能少,特别是指针和结构体等知识.这篇文章先回顾C语言基础,方便后续代码的阅读. 1 关于ELF文件 linux中的C编译得到的目标文件和可执行文件都是ELF格式的,可执行文件中以segment来划分,目标文件中,我们是以section划分.一个segment包含一个或多个section,通过readelf命令可以看到完整的section和segment信息.看一个栗子: char pear[40]; static

《python源码剖析》笔记 python虚拟机中的函数机制

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 1.Python虚拟机在执行函数调用时会动态地创建新的 PyFrameObject对象, 这些PyFrameObject对象之间会形成PyFrameObject对象链,模拟x86平台上运行时栈 2.PyFuctionObject对象 typedef struct { PyObject_HEAD PyObject *func_code: //对应函数编译后的PyCodeObject对象 Py

《python源码剖析》笔记 python中的整数对象

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 1. PyIntObject --> long的一个简单包装 typedef struct{ PyObject_HEAD long ob_ival; } PyIntObject; PyInt_Type --> PyIntObject的类型对象.与对象相关的元信息实际上都是保存在与对象对应的类型对象中的 PyTypeObject PyInt_Type = { PyObject_HEAD_I

python源码剖析

作为主流的动态语言,Python不仅简单易学.移植性好,而且拥有强大丰富的库的支持.此外,Python强大的可扩展性,让开发人员既可以非常容易地利用C/C++编写Python的扩展模块,还能将Python嵌入到C/C++程序中,为自己的系统添加动态扩展和动态编程的能力. 为了更好地利用Python语言,无论是使用Python语言本身,还是将Python与C/C++交互使用,深刻理解Python的运行原理都是非常重要的.<动态语言技术精品书廊·Python源码剖析:深度探索动态语言核心技术>以C

Python源码剖析 pdf下载

网盘下载:Python源码剖析 pdf下载 – 易分享电子书PDF资源网 作者: 陈儒 出版社: 电子工业出版社 副标题: 深度探索动态语言核心技术 出版年: 2008-6 页数: 480 定价: 69.80元 装帧: 平装 ISBN: 9787121068744 内容简介 · · · · · · 作为主流的动态语言,Python不仅简单易学.移植性好,而且拥有强大丰富的库的支持.此外,Python强大的可扩展性,让开发人员既可以非常容易地利用C/C++编写Python的扩展模块,还能将Pyth

Python源码剖析(高清版)PDF

Python源码剖析(高清版)PDF百度网盘链接:https://pan.baidu.com/s/1v0nalmMRYTJn1VTw-AHjxw 提取码:kjb4 复制这段内容后打开百度网盘手机App,操作更方便哦内容简介 · · · · · · 作为主流的动态语言,Python不仅简单易学.移植性好,而且拥有强大丰富的库的支持.此外,Python强大的可扩展性,让开发人员既可以非常容易地利用C/C++编写Python的扩展模块,还能将Python嵌入到C/C++程序中,为自己的系统添加动态扩展

《python源码剖析-字节码和虚拟机》

|   分类于 python源码剖析  | https://fanchao01.github.io/blog/categories/python%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90/ https://fanchao01.github.io/blog/2014/12/26/python-GIL/ python源码剖析-字节码和虚拟机 发表于 2016-11-13   |   分类于 python源码剖析  | Python会将代码先编译成字节码,然后在虚拟机中动

《python源码剖析》笔记 python虚拟机中的一般表达式

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 1.字节码指令 LOAD_CONST:从consts表中读取序号为i的元素并压入到运行时栈中 STORE_NAME:改变local名字空间.从符号表names取序号为i的元素作为变量名, 取运行时栈的栈顶元素作为变量值,完成从变量名到变量值的映射关系的创建. BUILD_MAP:创建一个空的PyDictObject对象,并压入运行时栈 DUP_TOP:将栈顶元素的引用计数增加1,并将它再次