python 内核分析(十):Python 虚拟机

本系列作为本人@takooctopus深入学习python机制的记录,这个博客遵照着栖迟于一丘的博客上面的流程进行的,也包含我在实际查看源码时的感想,特此列出,表示感谢。

Python虚拟机

之前生成的python字节码是无法直接运行的,这个时候我们需要使用python虚拟机,将编译得到的字节码进行执行。

而python虚拟机的实现,是以一种近似于CPU的操作方式——栈帧。

我们虚拟机其实在执行的时候,面对的是PyFrameObject,我们将其想象成一个空间就好

我们看其在[Include/frameobject.h]中的定义


{{- code -}}

明显的,这个能够形成一个链表,通过*f_back能够找到之前的栈帧。在执行完当前栈帧后能够返回之前的栈帧中。

*f_code就是PyCodeObject对象

*f_builtins, *f_globals*f_locals作为命名空间,维护了变量名和变量值之间的关系,而他们都是PyDictObject

**f_valuestack**f_stacktop记录了栈底和栈顶的地址

由于每次创建栈帧对象时,PyCodeObject的大小都可能不同,最终其大小也可能不同,占的大小装在PyCodeObject中的co_stacksize


PyFrameObject的创建

首先,我们看栈帧的创建


{{- code -}}

在这之中其调用了一个私有方法_PyFrame_New_NoTrack()


{{- code -}}

extras = code->co_stacksize + code->co_nlocals + ncells + nfrees; 中能看到栈帧的动态内存区由四个部分组成

ncells = PyTuple_GET_SIZE(code->co_cellvars);nfrees = PyTuple_GET_SIZE(code->co_freevars);我们知道额外的部分除了给PyCodeObject之外,还有给co_cellvarsco_freevars

而其f_valuestackf_stacktop分别表示栈底和栈顶


PyFrameObject 的访问

我们可以很简单地通过 sys.getframe() 函数获取当前所运行的栈帧对象


PyFrameObject 的 NAMESPACE

我们在PyFrameObject中能够看见的三个属性*f_builtins,*f_globalsf_locals分别代表了三个独立的命名空间NAMESPACE

在一般的python文件结构中,每个.py文件被视作一个module,我们把一个文件夹的所有.py文件看作一个模块,这个模块中有一个是主module,以python main.py进行加载,其余的用import引入。

而在.py文件里写的import foo foo.barfoo.bar作为属性引用,使用了另外一个NAMESPACE中的名字

这种名字的查找是有规律的,Python遵照了LEGB原则,即locals -> enclosing function -> globals -> __builtins__原则,以以下顺序依次查询

  • locals 是函数内的名字空间,包括局部变量和形参
  • enclosing 外部嵌套函数的名字空间(闭包中常见)
  • globals 全局变量,函数定义所在模块的名字空间
  • builtins 内置模块的名字空间

虚拟机运行框架

我们假设初始化已经完成,而现在第一个就是PyEval_EvalFrameEx()


{{- code -}}

在这里面调用了eval_frame(),我们看它定义的宏


{{- code -}}

知道其是通过PyEval_EvalFrameDefault()函数调用的

PyEval_EvalFrameDefault()的实现内容极多,一共几千行吧 😦


{{- code -}}

虚拟机从头开始遍历整个co_codefirst_instr 永远指向字节码指令序列的开始位置, next_instr 永远指向下一条待执行的字节码指令的位置, f_lasti 指向上一条已经执行过的字节码指令的位置

而整个架构就是switch (opcode) {}sitch/TARGET结构

在函数最开始的时候还方便的设置了几个关于tuple获取和code移动的宏


{{- code -}}

这里NEXTOPARG()中是用于读取下一条的,其读取操作码和参数


{{- code -}}

而关于其判断参数多少,在switch(opcode)之前有一个宏


{{- code -}}

这里HAS_ARG()的宏定义在[Include/opcode.h]中,在我们上次讲到了,是一个判断操作数是否大于这个阈值的数值判断。


关于Why

在进入switch(opcode)选择后,每个选项中都有一个why变量,其实用于指示退出python引擎的状态因为有可能出错,下面是初始化函数PyEval_EvalFrameDefault()对于错误why的一些处理


{{- code -}}

我们可以看出来why会有不同的状态返回,而具体的状态码,也在[Python/ceval.c]中定义了


{{- code -}}

python框架总的流程就是,执行一个无尽的for循环,取出第一条字节码之后, 判断指令后执行, 然后一条接一条的从字节流中获取。

进行完异常检查后,最后进行出栈操作:


{{- code -}}

Python的线程

可执行文件除开栈帧,还有很重要的:进程线程

对于一个单进程多线程的可执行文件,操作系统会创建一个进程和多个线程,但线程键共享global变量,切换线程需要保存工作环境以便恢复。

python实现了对多线程的支持, 而且python中的线程就是操作系统上的一个原生线程。 python虚拟机是对CPU的抽象, 在切换线程之前同样需要保存关于当前线程的信息。

python是使用PyThreadState对象对线程信息进行抽象。

  • 线程的抽象并非原始线程,python仍旧使用操作系统的原生线程
  • 而对于线程抽象的操作,使用PyInterpreterState来操作
  • python一般只维护一个PyInterpreterState,其中维护多个PyThreadState对象

而线程同步,少不了的就是锁机制「lock」,他们通过一个全局解释器锁 Global Interpreter Lock (GIL) 实现。

我们看PyInterpreterStatePyThreadState的实现:


{{- code -}}

{{- code -}}

明显的,每个进程下属一个解释器,下属一个线程池,进程间和线程间都有next属性通向下一个,而线程下维护一个栈帧表。