《深入Python虚拟机》笔记3-鸟瞰之2

一旦初始化完成, Py_Main函数调用main.c文件里的run_file函数。下面一些列函数将被调用:PyRun_AnyFileExFlags -> PyRun_SimpleFileExFlags->PyRun_FileExFlags->PyParser_ASTFromFileObject。

PyRun_SimpleFileExFlags会创建__main__名称空间,而且会检查是否pyc版本的文件存在-pyc是包含着编译过的内容。如果pyc版本文件存在的话,其可能会被当做二进制文件被读取和执行。如果没有pyc文件的话,PyRun_FileExFlags会被执行。

PyParser_ASTFromFileObject将会调用PyParser_ParseFileObject来读取模块内容,然后构建解析树。被创建好的解析树会被传递给PyParser_ASTFromNodeObject,从而创建出抽象语法树。

如果你读实际的虚拟机C源代码读到这里,你该遇到Py_INCREF和Py_DECREF了,它们都是内存管理函数。CPython使用引用计数来管理对象的生命周期。一旦新建引用指向对象,那么Py_INCREF会将对象的引用递增,一旦引用超出作用域,Py_INCREF就会递减对象的引用。

生成的抽象语法树被传递给run_mod函数,该函数滴调用PyAST_CompileObject函数来创建代码对象。注意,PyAST_CompileObject创建的字节码被传递给一个简单的peephole优化器,该优化器做很容易实现的字节码优化,然后才生成代码对象。

run_mod函数接下来调用ceval.c文件的PyEval_EvalCode来处理代码对象。这导致接下来的一系列函数调用:PyEval_EvalCode->PyEval_EvalCode->_PyEval_EvalCodeWithName->_PyEval_EvalFrameEx。代码对象作为一个参数传递给一个接一个的函数。_PyEval_EvalFrameEx是真正的解释器循环用来执行代码对象。但是,它不仅仅是用代码对象作为参数调用的,它的参数之一是具有引用代码对象的字段的帧对象。这个帧对象提供执行代码对象的上下文。简单地来说就是,解释器循环不断地从指令数组中读取指令计数器指向的下一个指令。然后执行该指令,为进程的值栈添加或者删除对象,直到没有指令可被执行或者某些异常出现打断循环。

Python提供一系列的函数,其中一个可以用来查看实际的代码对象。例如一个简单的程序可以被编译成代码对象,然后被反汇编从而获得Python虚拟机实际执行的操作码。下面是一个例子:

1 >>> def square(x):

2 … return x*x

3 …

4

5 >>> dis(square)

6 2 0 LOAD_FAST 0 (x)

7 2 LOAD_FAST 0 (x)

8 4 BINARY_MULTIPLY

9 6 RETURN_VALUE

./Include/opcodes.h包含python虚拟机里所有用到的指令和操作码,操作码概念上是非常简单的。以上面的代码为例:

LOAD_FAST:加载参数到值栈。python虚拟机是基于栈的虚拟机,所以这意味着操作码的评估是从栈上取值,然后结果会被放回栈中去。

BINARY_MULTIPLY:该操作码,将从值栈上pop两项,进行二进制乘法,然后将结果放回值栈。

RETURN VALUE:该指令从值栈上pop数据,将对象的返回值设为该值,然后打破解释器循环。

当所有的指令被执行完,Py_Main函数继续执行,但是这时开始清理进程。如同Py_Initialize在解释器开始阶段执行初始化一样,Py_FinalizeEx会进行清理工作。这些清理工作包括等待线程退出,调用那些退出钩子,然后释放解释器仍在使用的内存。