文章目录
Python虚拟机是Python的核心部分,它负责很多事情包括运行字节码等等,从前面我们知道,当Python的source code被编译成字节码指令,之后就由Python虚拟机负责根据上下文环境来执行这些指令,所以Python虚拟机在整个Python的环节中起核心作用。
1、Python虚拟机架构
Python虚拟机实际上就是模拟真实物理机的执行原理,因此在这之前我们先来了解一下普通的X86架构的机器执行方式。我们先来看看运行时栈的栈帧
我们来详细分析一下上述的图示结构,栈这种数据结构不用多说,它是一种先入后出的结构,那么对于较早的帧就是指更早之前调用的函数或者其他。而调用者的帧如果从函数调用的角度来看,我们可以这样理解,假如现在有一个函数 func1, 它现在执行到了函数内部的某个位置,那么调用者的帧就是当前函数func1的栈帧。而此时在这个位置上又调用了另一个函数func2,那么当前帧就是函数func2的栈帧,这个函数func2中的所有的局部变量都在自己的栈帧中,只有在发生函数调用的时候才会创建新的栈帧。因此,在func1中执行funct2调用的时候,就会在地址空间中func1的栈帧时候创建func2的栈帧。那么在函数调用完成之后怎么恢复到调用者原来的地方呢?在发生函数调用的时候,系统会保存上一个栈帧的栈指针esp和帧指针ebp,在函数调用完成后,这两个指针会恢复到调用前的值,这样在函数调用后又可以回到原来的地方继续执行。
我们知道在PyCodeObject对象中存储了字节码指令和静态信息,但是虚拟机仍然不能在这个对象上进行所有的动作,因为还有一个重要的东西没有在这个对象里面,这就是程序运行的动态信息----执行环境。
执行环境是什么呢?我们通过一个例子说明一下
str1 = 'hello world'
def func():
str1 = 'hello python'
print(str1)
func() # hello python
print(str1) # hello world
我们可以看到虽然都是打印变量str1的值,它们在PyCodeObject中的字节码的指令是一样的,但是在不同的地方却打印出了不同的值,这就是因为执行环境的影响。像这样同样的符号在不同的地方对应不同的值或者不同类型这样的情况,只能在运行期间动态捕捉和维护,这是不可能在PyCodeObject中被静态存储的。这是不是和我们之前提到的命名空间很相似,实际上命名空间只是执行环境的一部分,因此我们可以简单地总结出对于一个py文件的执行过程,当Python开始执行py文件中的第一条表达式的时候,会首先创建一个执行环境,所有的字节码指令均在这个执行环境中执行,当发生函数调用的时候,就会在上一个执行环境之外创建一个新的执行环境。因此在Python运行的时候,虚拟机真正处理的是一个PyFrameObject这就是执行环境,而它就是对x86架构的栈帧模拟。
PyFrameObject对象中包含很多的东西,有命名空间,字节码指令等等,我们现在就来看看它的定义:
typedef struct _frame {
// 变长对象的头部
PyObject_VAR_HEAD
// 上一级的栈帧
struct _frame *f_back; /* previous frame, or NULL */
// PyCodeObject对象
PyCodeObject *f_code; /* code segment */
// builtin命名空间,是PyDictObject对象
PyObject *f_builtins; /* builtin symbol table (PyDictObject) */
// global命名空间,PyDictObject对象
PyObject *f_globals; /* global symbol table (PyDictObject) */
// local命名空间,PyDictObject对象
PyObject *f_locals; /* local symbol table (any mapping) */
// 运行时栈底
PyObject **f_valuestack; /* points after the last local */
/* Next free slot in f_valuestack. Frame creation sets to f_valuestack.
Frame evaluation usually NULLs it, but a frame that yields sets it
to the current stack top. */
// 运行时栈顶
PyObject **f_stacktop;
// 回溯函数,可用于打印异常信息
PyObject *f_trace