• Python之PyFrameObject动态执行环境


    Python虚拟机中的执行环境

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

    图1-1

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

    #include <stdio.h>
    void f(int a, int b)
    {
        printf("a=%d, b=%d
    ", 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的各个名字空间

  • 相关阅读:
    [linux]w命令和uptime命令查看系统负载
    Gherkin关键字
    Navicat Premium常用快捷键
    linux如何手动释放linux内存
    IDEA报compilation failed:internal java compiler error解决方法
    App功能测试的7大注意点
    手术切口类别
    JAVA中关于Map的九大问题
    Web前端开发规范文档
    Java6 WebService的发布
  • 原文地址:https://www.cnblogs.com/beiluowuzheng/p/9383603.html
Copyright © 2020-2023  润新知