Python虚拟机中的执行环境

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

图1-1

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

  1. #include <stdio.h>
  2. void f(int a, int b)
  3. {
  4. printf("a=%d, b=%d\n", a, b);
  5. }
  6. void g()
  7. {
  8. f(1, 2);
  9. }
  10. main(int argc, char const *argv[])
  11. {
  12. g();
  13. return 0;
  14. }

  

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

  1. i = "Python"
  2.  
  3. def f():
  4. i = 999
  5. print(i) # <1>
  6.  
  7. f()
  8. 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等一些基础类型和基础函数
  1. x = 3
  2.  
  3. def f1(x=1):
  4. y = 2
  5. print("locals:", locals())
  6.  
  7. f1()
  8. print("globals:", globals())

  

运行结果:

  1. locals: {'y': 2, 'x': 1}
  2. 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

  1. typedef struct _frame {
  2. PyObject_VAR_HEAD
  3. struct _frame *f_back; /* 执行环境链上的前一个frame */
  4. PyCodeObject *f_code; /* PyCodeObject对象 */
  5. PyObject *f_builtins; /* builtin名字空间 */
  6. PyObject *f_globals; /* global名字空间 */
  7. PyObject *f_locals; /* local名字空间 */
  8. PyObject **f_valuestack; /* 运行时的栈底位置 */
  9. PyObject **f_stacktop; /* 运行时的栈顶位置 */
  10. …………
  11. int f_lasti; /* 上一条字节码指令在f_code中的偏移位置 */
  12. /* As of 2.3 f_lineno is only valid when tracing is active (i.e. when
  13. f_trace is set) -- at other times use PyCode_Addr2Line instead. */
  14. int f_lineno; /* 当前字节码对应的源代码行 */
  15. int f_iblock; /* index in f_blockstack */
  16. …………
  17. //动态内存、维护(局部变量+cell对象集合+free对象集合+运行时栈)所需要的空间
  18. PyObject *f_localsplus[1];
  19. } 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维护着一段变动长度的内存,但这段内存并不只是给栈使用,还有别的对象也会使用

  1. PyFrameObject *
  2. PyFrame_New(PyThreadState *tstate, PyCodeObject *code, PyObject *globals,
  3. PyObject *locals)
  4. {
  5. PyFrameObject *back = tstate->frame;
  6. PyFrameObject *f;
  7. PyObject *builtins;
  8. Py_ssize_t i;
  9.  
  10. if (code->co_zombieframe != NULL) {
  11. f = code->co_zombieframe;
  12. code->co_zombieframe = NULL;
  13. _Py_NewReference((PyObject *)f);
  14. assert(f->f_code == code);
  15. }
  16. else {
  17. Py_ssize_t extras, ncells, nfrees;
  18. ncells = PyTuple_GET_SIZE(code->co_cellvars);
  19. nfrees = PyTuple_GET_SIZE(code->co_freevars);
  20. //四分部构成PyFrameObject维护的动态内存区,其大小由extras决定
  21. extras = code->co_stacksize + code->co_nlocals + ncells + nfrees;
  22. //计算初始化时运行时栈的栈顶
  23. extras = code->co_nlocals + ncells + nfrees;
  24. f->f_valuestack = f->f_localsplus + extras;
  25. }
  26. f->f_stacktop = f->f_valuestack;
  27.  
  28. return f;
  29. }

  

从上面的代码可以知道,创建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方法

  1. import sys
  2.  
  3. value = 3
  4.  
  5. def g():
  6. frame = sys._getframe()
  7. print("current function is:", frame.f_code.co_name)
  8. caller = frame.f_back
  9. print("caller function is:", caller.f_code.co_name)
  10. print("caller's local namespace:", caller.f_locals)
  11. print("caller's global namespace:", caller.f_globals.keys())
  12.  
  13. def f():
  14. a = 1
  15. b = 2
  16. g()
  17.  
  18. def show():
  19. f()
  20.  
  21. show()

  

运行结果:

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

  

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

Python之PyFrameObject动态执行环境的更多相关文章

  1. Python基础:27执行环境

    一:可调用对象 可调用对象,是任何能通过函数操作符“()”来调用的对象.Python 有4 种可调用对象:函数,方法,类,以及一些类的实例. 1:函数 python 有 3 种不同类型的函数对象. a ...

  2. 配置EditPlus编辑器使其成为Python的编辑、执行环境

    1.添加Python群组 运行EditPlus,选择工具→配置用户工具进入参数设置框. 单击添加工具→应用程序.菜单文字输入python,命令为Python的安装路径,参数输入 $(FileName) ...

  3. Python入门笔记(26):Python执行环境

    一.python特定的执行环境 在当前脚本继续进行 创建和管理子进程 执行外部命令或程序 执行需要输入的命令 通过网络来调用命令 执行命令来创建需要处理的输出 动态生成Python语句 导入Pytho ...

  4. Python学习(25):Python执行环境

    转自 http://www.cnblogs.com/BeginMan/p/3191856.html 一.python特定的执行环境 在当前脚本继续进行 创建和管理子进程 执行外部命令或程序 执行需要输 ...

  5. python执行环境

    转自 http://www.cnblogs.com/BeginMan/p/3191856.html 一.python特定的执行环境 在当前脚本继续进行 创建和管理子进程 执行外部命令或程序 执行需要输 ...

  6. 第6.6节 Python动态执行小结

    一.    Python动态执行支持通过输入数据流或文件传入Python源代码串,进行编译后执行,可以通过这种方式扩展Python程序的功能: 二.    动态执行方法可能导致恶意攻击,因此使用时需要 ...

  7. 第6.2节 Python特色的动态可执行方法简介

    一.    基本概念 动态可执行,是指在代码中通过外部输入或代码嵌入的常量字符串包含代码的方式提供Python代码,要求Python执行这些代码.这样就可以达到开放式运行的效果,提高程序的能力和灵活性 ...

  8. Python核心编程读笔 13:执行环境

    第14章  执行环境 一.可调用对象 python有四种可调用对象:函数.方法.类.一些类的实例 1 函数 (1)内建函数(BIF) BIF是用c/c++写的,编译后放入python解释器,然后把它们 ...

  9. django系列5.4--ORM中执行原生SQL语句, Python脚本中调用django环境

    ORM执行原生sql语句 在模型查询API不够用的情况下,我们还可以使用原始的SQL语句进行查询. Django 提供两种方法使用原始SQL进行查询:一种是使用raw()方法,进行原始SQL查询并返回 ...

随机推荐

  1. 2015博客升级记(五):CentOS 7.1编译安装PHP7

    https://typecodes.com/web/centos7compilephp7.html

  2. 问题:java.sql.SQLException: No value specified for parameter 1

    解决方案:没有指定参数 String user = req.getParameter("user"); String pwd = req.getParameter("pw ...

  3. td 内容自动换行 table表格td设置宽度后文字太多自动换行

    设置table 的 style="table-layout:fixed;" 然后设置td的 style="word-wrap:break-word;" 即可   ...

  4. SQL数据库基础二

  5. JavaScript笔记6-数组新方法

    七.ECMAScript5关于数组的新方法 1.forEach():遍历数组,并为每个元素调用传入的函数;     举例:    var a = [1,2,3]; var sum = 0; //传一个 ...

  6. jenkins只能同时构建2个Job怎么办?

    在jenkins 构建任务时,同时只能构建2个,如果两个没有job没有结束,构建第3个就会不执行: 提示: pending—Waiting for next available executor on ...

  7. Linux OpenGL 实践篇-16 文本绘制

    文本绘制 本文主要射击Freetype的入门理解和在OpenGL中实现文字的渲染. freetype freetype的官网,本文大部分内容参考https://www.freetype.org/fre ...

  8. AWVS12 防止反复注册

    以管理员权限运行cmd,输入以下内容: cacls "C:\ProgramData\Acunetix\shared\license." /t /p everyone:r 如图:

  9. Acronis.Disk.Director磁盘分区管理

    Acronis.Disk.Director分为for 专业版和服务器版的,我在生产环境中调整Windows2003跳板机使用的是Acronis.Disk.Director Server 10.0.20 ...

  10. Ubuntu 下安装WPS

    1.先到wps官网上下载wps的deb包. http://www.wps.cn/product/ 2.我使用的64位的,所以得安装32位兼容包 sudo apt-get install ia32-li ...