Python之PyFrameObject动态执行环境

Python虚拟机中的执行环境

Python的虚拟机实际上是在模拟操作系统运行可执行文件的过程,首先,我们先来讲一下普通的x86的机器上,可执行文件是以一种什么方式运行的。

图1-1

图1-1所展示的运行时栈的情形可以看作是如下的C代码运行时情形:

#include <stdio.h>
void f(int a, int b)
{
    printf("a=%d, b=%d\n", a, b);
}
void g()
{
    f(1, 2);
}
main(int argc, char const *argv[])
{
    g();
    return 0;
}

  

esp:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶.

ebp:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部

当程序的流程进入函数f时,图1-1,其中“调用者的帧”是函数g的栈帧,而“当前帧”则是函数f的栈帧。对于一个函数而言,其所有局部变量的操作都在自己的栈帧中完成,而函数间的调用则通过创建新的栈帧完成

图1-1所示的系统中,运行时栈是从地址空间的高地址向低地址延伸的。当在函数g中执行函数f的调用时,系统就会在地址空间中,于g的栈帧之后,创建f的栈帧。当然,在发生函数调用时,系统会保存上一个栈帧的栈指针esp和帧指针ebp。当函数f执行完成之后,系统会把esp和ebp的值恢复为创建f的栈帧之前的值。这样,程序的流程又回到函数g中,而程序的工作空间则又回到函数g的栈帧中。这就是可执行文件再x86机器上的大致运行原理。而Python正是在虚拟机中通过不同的实现方式模拟了这一原理,从而完成了Python字节码指令序列的执行。

我们之前在Python之code对象与pyc文件(一)Python之code对象与pyc文件(二)Python之code对象与pyc文件(三)中剖析了,PyCodeObject对象包含了程序中的静态信息,然而有一点PyCodeObject对象没有包含,那就是关于程序运行时的动态信心——执行环境

什么是执行环境呢?考虑下面的一个例子:

env.py

i = "Python"


def f():
    i = 999
    print(i)  # <1>


f()
print(i)  # <2>

  

在代码<1>和<2>两个地方,都执行了同样的动作,打印变量i的值。显然,它们所对应的字节码指令肯定是相同的,但这两条语句的执行效果肯定不同。正是因为执行环境的影响,所以在<1>处会打印999,在<2>处会打印Python。像这种同样的符号在程序运行的不同时刻对应不同的值,甚至不同类型的情况,必须在运行时动态的捕捉和维护。这些信息是不可能在PyCodeObject对象中被静态存储的

这里简单介绍Python中的一个名词——名字空间:

  • python中,每个函数都有一个自己的名字空间,通过locals()访问,它记录了函数的变量
  • python中,每个module有一个自己的名字空间,通过globals()访问,它记录了module的变量,包括 functions, classes 和其它imported modules,还有 module级别的变量和常量
  • 还有一个builtins名字空间,可以被任意模块访问,这个builtins名字空间存储着 class object、class Exception、def len等一些基础类型和基础函数
x = 3


def f1(x=1):
    y = 2
    print("locals:", locals())


f1()
print("globals:", globals())

  

运行结果:

locals: {'y': 2, 'x': 1}
globals: {'__name__': '__main__',…………, 'x': 3, 'f1': <function f1 at 0x000000DA28CE3E18>}

  

名字空间是执行环境的一部分,除了名字空间,在执行环境中,还包含了一些其他信息

结合x86平台运行可执行文件的机理,我们可以用这样的机理来解释env.py的执行过程。当Python开始执行env.py中第一条表达式时,Python已经建立起一个执行环境A,所有的字节码指令都会在这个执行环境中执行。Python可以从这个执行环境中获取变量的值,也可以根据字节码的指令修改执行环境中某个变量的值,以影响后续程序的运行。这样的过程会一直持续下去,直到发生了函数的调用行为

当Python在执行环境A中执行调用函数f的字节码指令时,会在当前的执行环境A之外重新创建一个新的执行环境B,在这个新的执行环境B中,有一个新的名字为"i"的对象。所以,新的执行环境B可以对应图1-1这种所示的新的栈帧

所以在Python真正执行的时候,它的虚拟机实际上面对的并不是一个PyCodeObject对象,而是另外一个对象——PyFrameObject,它就是我们所说的执行环境,也是Python对x86平台上栈帧的模拟

Python源码中的PyFrameObject

Python源码中PyFrameObject的定义:

frameobject.h

typedef struct _frame {
    PyObject_VAR_HEAD
    struct _frame *f_back;	/* 执行环境链上的前一个frame */
    PyCodeObject *f_code;	/* PyCodeObject对象 */
    PyObject *f_builtins;	/* builtin名字空间 */
    PyObject *f_globals;	/* global名字空间 */
    PyObject *f_locals;		/* local名字空间 */
    PyObject **f_valuestack;	/* 运行时的栈底位置 */
    PyObject **f_stacktop;  /* 运行时的栈顶位置 */
    …………
    int f_lasti;		/* 上一条字节码指令在f_code中的偏移位置 */
    /* As of 2.3 f_lineno is only valid when tracing is active (i.e. when
       f_trace is set) -- at other times use PyCode_Addr2Line instead. */
    int f_lineno;		/* 当前字节码对应的源代码行 */
    int f_iblock;		/* index in f_blockstack */
	…………
	//动态内存、维护(局部变量+cell对象集合+free对象集合+运行时栈)所需要的空间
    PyObject *f_localsplus[1];	
} PyFrameObject;

  

从f_back我们可以看出一点,在Python实际执行的过程中,会产生很多PyFrameObject对象,而这些对象会被链接起来,形成一条执行环境链表。这正是对x86机器上栈帧间关系的模拟。在x86上,栈帧间通过esp指针和ebp指针建立关系,使得新栈帧结束后能返回旧栈帧中,而Python正是靠f_back来完成这个动作的

在f_code中存放的是一个待执行的PyCodeObject对象,而接下来的f_builtins、f_globals、f_locals是3个独立的名字空间,如我们所说,名字空间是执行环境的一部分。当执行env.py时,当要打印i这个变量,会去f_locals中寻找i这个PyStringObject变量,找到后将其对应的值取出,再打印出来

在PyFrameObject开头,有一个PyObject_VAR_HEAD,这表明PyFrameObject是一个变长对象,即每次创建PyFrameObject对象的大小可能是不一样的,这些变动的内存是用来做什么呢?实际上,每一个PyFrameObject对象都维护着一个PyCodeObject对象。这表明每一个PyFrameObject对象和Python源码中的一段code都是对应的,更准确的说,是和我们研究PyCodeObject时提到的Code Block对应的。而在编译一段Code Block时,会计算出这段Code Block执行过程中所需要的栈空间大小。这个栈空间大小存储在PyCodeObject的co_stacksize中。因为不同的Code Block在执行时所需的栈空间大小不同,所以决定PyFrameObject的开头一定有一个PyObject_VAR_HEAD

PyFrameObject对象是对x86机器上单个栈帧活动的模拟,既然在x86的单个栈帧中,包含了计算所需的内存空间,为什么执行计算还需要内存空间呢?举个例子:在计算c=a+b时,我们需要将a和b的值读入内存,然后计算结果也要存放在内存中,这些内存就是执行计算所必须的内存,然后计算结果也要存放在内存中,这些内存就是执行计算所必须的内存。所以,作为对x86栈帧的模拟,在PyFrameObject中,也提供了这些对内存空间的模拟。这里,我们称为“运行时栈”。注意:这里的“运行时栈”的概念和x86平台上的“运行时栈”有所不同,我们这里所谓的“运行时栈”单指运算时所需的内存空间

  

图1-2

图1-2展示了Python虚拟机在某个运行时刻的完整运行环境

 

PyFrameObject中的动态内存空间

在PyFrameObject对象所维护的运行时栈中,存储的都是PyObject *,f_localsplus维护着一段变动长度的内存,但这段内存并不只是给栈使用,还有别的对象也会使用

PyFrameObject *
PyFrame_New(PyThreadState *tstate, PyCodeObject *code, PyObject *globals,
	    PyObject *locals)
{
	PyFrameObject *back = tstate->frame;
	PyFrameObject *f;
	PyObject *builtins;
	Py_ssize_t i;

	if (code->co_zombieframe != NULL) {
                f = code->co_zombieframe;
                code->co_zombieframe = NULL;
                _Py_NewReference((PyObject *)f);
                assert(f->f_code == code);
	}
	else {
		Py_ssize_t extras, ncells, nfrees;
		ncells = PyTuple_GET_SIZE(code->co_cellvars);
		nfrees = PyTuple_GET_SIZE(code->co_freevars);
		//四分部构成PyFrameObject维护的动态内存区,其大小由extras决定
		extras = code->co_stacksize + code->co_nlocals + ncells + nfrees;
		//计算初始化时运行时栈的栈顶
		extras = code->co_nlocals + ncells + nfrees;
		f->f_valuestack = f->f_localsplus + extras;
	}
	f->f_stacktop = f->f_valuestack;

	return f;
} 

  

从上面的代码可以知道,创建PyFrameObject对象时,额外申请的那部分内存中有一部分是给PyCodeObject对象中存储的那些局部变量:co_freevars、co_cellvars。而另一部分才是给运行时栈使用的。所以,PyFrameObject对象中栈的起始位置(也就是栈底)是又f_valuestack维护的,而f_stacktop维护额当前的栈顶

图1-3

图1-3是一个刚被创建的PyFrameObject对象的示意图,,从中可以看到运行时栈和PyFrameObject对象中动态内存部分的关系

在Python中访问PyFrameObject对象

在Python中,有一种frame object,它是对C一级的PyFrameObject的包装,而且Python还提供了一个方法能方便地获得当前处于活动状态的frame object。这个方法就是sys module中的_getframe方法

import sys

value = 3


def g():
    frame = sys._getframe()
    print("current function is:", frame.f_code.co_name)
    caller = frame.f_back
    print("caller function is:", caller.f_code.co_name)
    print("caller's local namespace:", caller.f_locals)
    print("caller's global namespace:", caller.f_globals.keys())


def f():
    a = 1
    b = 2
    g()


def show():
    f()


show()

  

运行结果:

current function is: g
caller function is: f
caller's local namespace: {'b': 2, 'a': 1}
caller's global namespace: dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', '__file__', '__cached__', 'sys', 'value', 'g', 'f', 'show'])

  

从执行结果可以看到,在函数f中可以通过caller完全获得其调用者g函数的信息,甚是是g的各个名字空间

posted @ 2018-07-29 10:43  北洛  阅读(1782)  评论(0编辑  收藏  举报