深入理解python虚拟机:程序执行的载体——栈帧

栈帧(Stack Frame)是 Python 虚拟机中程序执行的载体之一,也是 Python 中的一种执行上下文。每当 Python 执行一个函数或方法时,都会创建一个栈帧来表示当前的函数调用,并将其压入一个称为调用栈(Call Stack)的数据结构中。调用栈是一个后进先出(LIFO)的数据结构,用于管理程序中的函数调用关系。

栈帧的创建和销毁是动态的,随着函数的调用和返回而不断发生。当一个函数被调用时,一个新的栈帧会被创建并推入调用栈,当函数调用结束后,对应的栈帧会从调用栈中弹出并销毁。

栈帧的使用使得 Python 能够实现函数的嵌套调用和递归调用。通过不断地创建和销毁栈帧,Python 能够跟踪函数调用关系,保存和恢复局部变量的值,实现函数的嵌套和递归执行。同时,栈帧还可以用于实现异常处理、调试信息的收集和优化技术等。

需要注意的是,栈帧是有限制的,Python 解释器会对栈帧的数量和大小进行限制,以防止栈溢出和资源耗尽的情况发生。在编写 Python 程序时,合理使用函数调用和栈帧可以帮助提高程序的性能和可维护性。

栈帧数据结构

 
typedef struct _frame {
PyObject_VAR_HEAD
struct _frame *f_back; /* previous frame, or NULL */
PyCodeObject *f_code; /* code segment */
PyObject *f_builtins; /* builtin symbol table (PyDictObject) */
PyObject *f_globals; /* global symbol table (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; /* Trace function */
/* In a generator, we need to be able to swap between the exception
state inside the generator and the exception state of the calling
frame (which shouldn't be impacted when the generator "yields"
from an except handler).
These three fields exist exactly for that, and are unused for
non-generator frames. See the save_exc_state and swap_exc_state
functions in ceval.c for details of their use. */
PyObject *f_exc_type, *f_exc_value, *f_exc_traceback;
/* Borrowed reference to a generator, or NULL */
PyObject *f_gen;
int f_lasti; /* Last instruction if called */
/* Call PyFrame_GetLineNumber() instead of reading this field
directly. As of 2.3 f_lineno is only valid when tracing is
active (i.e. when f_trace is set). At other times we use
PyCode_Addr2Line to calculate the line from the current
bytecode index. */
int f_lineno; /* Current line number */
int f_iblock; /* index in f_blockstack */
char f_executing; /* whether the frame is still executing */
PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
PyObject *f_localsplus[1]; /* locals+stack, dynamically sized */
} PyFrameObject;

内存申请和栈帧的内存布局

在 cpython 当中,当我们需要申请一个 frame object 对象的时候,首先需要申请内存空间,但是在申请内存空间的时候并不是单单申请一个 frameobject 大小的内存,而是会申请额外的内存空间,大致布局如下所示。

  • f_localsplus,这是一个数组用户保存函数执行的 local 变量,这样可以直接通过下标得到对应的变量的值。
  • ncells 和 nfrees,这个变量和我们前面在分析 code object 的函数闭包相关,ncells 和 ncells 分别表示 cellvars 和 freevars 中变量的个数。
  • stack,这个变量就是函数执行的时候函数的栈帧,这个大小在编译期间就可以确定因此可以直接确定栈空间的大小。

下面是在申请 frame object 的核心代码:

 
Py_ssize_t extras, ncells, nfrees;
ncells = PyTuple_GET_SIZE(code->co_cellvars); // 得到 co_cellvars 当中元素的个数 没有的话则是 0
nfrees = PyTuple_GET_SIZE(code->co_freevars); // 得到 co_freevars 当中元素的个数 没有的话则是 0
// extras 就是表示除了申请 frame object 自己的内存之后还需要额外申请多少个 指针对象
// 确切的带来说是用于保存 PyObject 的指针
extras = code->co_stacksize + code->co_nlocals + ncells +
nfrees;
if (free_list == NULL) {
f = PyObject_GC_NewVar(PyFrameObject, &PyFrame_Type,
extras);
if (f == NULL) {
Py_DECREF(builtins);
return NULL;
}
}
// 这个就是函数的 code object 对象 将其保存到栈帧当中 f 就是栈帧对象
f->f_code = code;
extras = code->co_nlocals + ncells + nfrees;
// 这个就是栈顶的位置 注意这里加上的 extras 并不包含栈的大小
f->f_valuestack = f->f_localsplus + extras;
// 对额外申请的内存空间尽心初始化操作
for (i=0; i<extras; i++)
f->f_localsplus[i] = NULL;
f->f_locals = NULL;
f->f_trace = NULL;
f->f_exc_type = f->f_exc_value = f->f_exc_traceback = NULL;
f->f_stacktop = f->f_valuestack; // 将栈顶的指针指向栈的起始位置
f->f_builtins = builtins;
Py_XINCREF(back);
f->f_back = back;
Py_INCREF(code);
Py_INCREF(globals);
f->f_globals = globals;
/* Most functions have CO_NEWLOCALS and CO_OPTIMIZED set. */
if ((code->co_flags & (CO_NEWLOCALS | CO_OPTIMIZED)) ==
(CO_NEWLOCALS | CO_OPTIMIZED))
; /* f_locals = NULL; will be set by PyFrame_FastToLocals() */
else if (code->co_flags & CO_NEWLOCALS) {
locals = PyDict_New();
if (locals == NULL) {
Py_DECREF(f);
return NULL;
}
f->f_locals = locals;
}
else {
if (locals == NULL)
locals = globals;
Py_INCREF(locals);
f->f_locals = locals;
}
f->f_lasti = -1;
f->f_lineno = code->co_firstlineno;
f->f_iblock = 0;
f->f_executing = 0;
f->f_gen = NULL;

现在我们对 frame object 对象当中的各个字段进行分析,说明他们的作用:

  • PyObject_VAR_HEAD:表示对象的头部信息,包括引用计数和类型信息。
  • f_back:前一个栈帧对象的指针,或者为NULL。
  • f_code:指向 PyCodeObject 对象的指针,表示当前帧执行的代码段。
  • f_builtins:指向 PyDictObject 对象的指针,表示当前帧的内置符号表,字典对象,键是字符串,值是对应的 python 对象。
  • f_globals:指向 PyDictObject 对象的指针,表示当前帧的全局符号表。
  • f_locals:指向任意映射对象的指针,表示当前帧的局部符号表。
  • f_valuestack:指向当前帧的值栈底部的指针。
  • f_stacktop:指向当前帧的值栈顶部的指针。
  • f_trace:指向跟踪函数对象的指针,用于调试和追踪代码执行过程,这个字段我们在后面的文章当中再进行分析。
  • f_exc_type、f_exc_value、f_exc_traceback:这个字段和异常相关,在函数执行的时候可能会产生错误异常,这个就是用于处理异常相关的字段。
  • f_gen:指向当前生成器对象的指针,如果当前帧不是生成器,则为NULL。
  • f_lasti:上一条指令在字节码当中的下标。
  • f_lineno:当前执行的代码行号。
  • f_iblock:当前执行的代码块在f_blockstack中的索引,这个字段也主要和异常的处理有关系。
  • f_executing:表示当前帧是否仍在执行。
  • f_blockstack:用于try和loop代码块的堆栈,最多可以嵌套 CO_MAXBLOCKS 层。
  • f_localsplus:局部变量和值栈的组合,是一个动态大小的数组。

如果我们在一个函数当中调用另外一个函数,这个函数再调用其他函数就会形成函数的调用链,就会形成下图所示的链式结构。

例子分析

我们现在来模拟一下下面的函数的执行过程。

 
import dis
def foo():
a = 1
b = 2
return a + b
if __name__ == '__main__':
dis.dis(foo)
print(foo.__code__.co_stacksize)
foo()

上面的 foo 函数的字节码如下所示:

 
6 0 LOAD_CONST 1 (1)
2 STORE_FAST 0 (a)
7 4 LOAD_CONST 2 (2)
6 STORE_FAST 1 (b)
8 8 LOAD_FAST 0 (a)
10 LOAD_FAST 1 (b)
12 BINARY_ADD
14 RETURN_VALUE

函数 foo 的 stacksize 等于 2 。

初始时 frameobject 的布局如下所示:

现在执行第一条指令 LOAD_CONST 此时的 f_lasti 等于 -1,执行完这条字节码之后栈帧情况如下:

在执行完这条字节码之后 f_lasti 的值变成 0。字节码 LOAD_CONST 对应的 c 源代码如下所示:

 
TARGET(LOAD_CONST) {
PyObject *value = GETITEM(consts, oparg); // 从常量表当中取出下标为 oparg 的对象
Py_INCREF(value);
PUSH(value);
FAST_DISPATCH();
}

首先是从 consts 将对应的常量拿出来,然后压入栈空间当中。

再执行 STORE_FAST 指令,这个指令就是将栈顶的元素弹出然后保存到前面提到的 f_localsplus 数组当中去,那么现在栈空间是空的。STORE_FAST 对应的 c 源代码如下:

 
TARGET(STORE_FAST) {
PyObject *value = POP(); // 将栈顶元素弹出
SETLOCAL(oparg, value); // 保存到 f_localsplus 数组当中去
FAST_DISPATCH();
}

执行完这条指令之后 f_lasti 的值变成 2 。

接下来的两条指令和上面的一样,就不做分析了,在执行完两条指令,f_lasti 变成 6 。

接下来两条指令分别将 a b 加载进入栈空间单中现在栈空间布局如下所示:

然后执行 BINARY_ADD 指令 弹出栈空间的两个元素并且把他们进行相加操作,最后将得到的结果再压回栈空间当中。

 
TARGET(BINARY_ADD) {
PyObject *right = POP();
PyObject *left = TOP();
PyObject *sum;
if (PyUnicode_CheckExact(left) &&
PyUnicode_CheckExact(right)) {
sum = unicode_concatenate(left, right, f, next_instr);
/* unicode_concatenate consumed the ref to left */
}
else {
sum = PyNumber_Add(left, right);
Py_DECREF(left);
}
Py_DECREF(right);
SET_TOP(sum); // 将结果压入栈中
if (sum == NULL)
goto error;
DISPATCH();
}

最后执行 RETURN_VALUE 指令将栈空间结果返回。

总结

在本篇文章当中主要介绍了 cpython 当中的函数执行的时候的栈帧结构,这里面包含的程序执行时候所需要的一些必要的变量,比如说全局变量,python 内置的一些对象等等,同时需要注意的是 python 在查询对象的时候如果本地 f_locals 没有找到就会去全局 f_globals 找,如果还没有找到就会去 f_builtins 里面的找,当一个程序返回的时候就会找到 f_back 他上一个执行的栈帧,将其设置成当前线程正在使用的栈帧,这就完成了函数的调用返回,关于这个栈帧还有一些其他的字段我们没有谈到在后续的文章当中将继续深入其中一些字段。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/83385.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

docker学习(十五)docker安装MongoDB

什么是MongoDB? MongoDB 是一个开源的、面向文档的 NoSQL 数据库管理系统&#xff0c;它以高性能、灵活的数据存储方式而闻名。与传统的关系型数据库不同&#xff0c;MongoDB 采用了一种称为 BSON&#xff08;Binary JSON&#xff09;的二进制 JSON 格式来存储数据。它是一种非…

(详解踩坑)GIT版本回滚git stash、git reset、git reset --hard、git revert

目录 背景 一、&#xff08;git log、git reflog&#xff09;查看git提交日志及命令历史 1.1 git log&#xff08;提交日志&#xff09; 1.2 git reflog&#xff08;命令历史&#xff09; 二、git reset&#xff08;回退到指定的版本&#xff0c;并且保留更改&#xff09; …

IDEA启动报错【java.sql.SQLSyntaxErrorException: ORA-00904: “P“.“PRJ_NO“: 标识符无效】

IDEA报错如下&#xff1a; 2023-08-17 11:26:15.535 ERROR [egrant-biz,b48324d82fe23753,b48324d82fe23753,true] 24108 --- [ XNIO-1 task-1] c.i.c.l.c.RestExceptionController : 服务器异常org.springframework.jdbc.BadSqlGrammarException: ### Error queryin…

leetcode 542. 01 Matrix(01矩阵)

矩阵中只有0&#xff0c;1值&#xff0c;返回每个cell到最近的0的距离。 思路&#xff1a; 0元素到它自己的距离是0&#xff0c; 只需考虑1到最近的0是多少距离。 BFS. 先把元素1处的距离更新为无穷大。 0的位置装入queue。 从每个0出发&#xff0c;走上下左右4个方向&…

axios / fetch 实现 stream 流式请求

axios 是一个支持node端和浏览器端的易用、简洁且高效的http库。本文主要介绍 axios 如何实现 stream 流式请求&#xff0c;注意这里需要区分 node 环境和浏览器环境。 一、node端 代码演示&#xff1a; const axios require(axios);axios({method: get,url: http://tiven.c…

【C# 基础精讲】使用async和await进行异步编程

在C#中&#xff0c;使用async和await关键字进行异步编程是一种强大的工具&#xff0c;可以在不阻塞主线程的情况下执行耗时操作&#xff0c;提高程序的并发性和响应性。本文将深入探讨async和await的基本概念、使用场景、编码规范以及一些示例&#xff0c;以帮助您更好地理解如…

RocketMQ双主双从同步集群部署

&#x1f388; 作者&#xff1a;互联网-小啊宇 &#x1f388; 简介&#xff1a; CSDN 运维领域创作者、阿里云专家博主。目前从事 Kubernetes运维相关工作&#xff0c;擅长Linux系统运维、开源监控软件维护、Kubernetes容器技术、CI/CD持续集成、自动化运维、开源软件部署维护…

定位服务器CPU爆满的具体原因

1、查询CPU消耗的进程 使用top命令查看系统的CPU和内存使用情况 CPU一列是线程占用百分比 2、具体查看某个占分比大的进程 以为PId:7355为例&#xff0c; 执行top -Hp 7355&#xff0c;线程按照CPU使用率排序。 3、将线程PID转化为16进制 执行printf %x 7391&#xff0c;将…

不含数字的webshell绕过

异或操作原理 1.首先我们得了解一下异或操作的原理 在php中&#xff0c;异或操作是两个二进制数相同时&#xff0c;异或(相同)为0&#xff0c;不同为1 举个例子 A的ASCII值是65&#xff0c;对应的二进制值是0100 0001 的ASCII值是96&#xff0c;对应的二进制值是 0110 000…

pdf格式文件下载不预览,云存储的跨域解决

需求背景 后端接口中返回的是pdf文件路径比如&#xff1a; pdf文件路径 &#xff08;https://wangzhendongsky.oss-cn-beijing.aliyuncs.com/wzd-test.pdf&#xff09; 前端适配是这样的 <ahref"https://wangzhendongsky.oss-cn-beijing.aliyuncs.com/wzd-test.pdf&…

Vscode详细安装教程

Vscode官网下载 官网地址&#xff1a;Download Visual Studio Code - Mac, Linux, Windows 通过链接可以直接跳转到下面的页面当中&#xff0c;支持的版本有Windows、Linux、Mac&#xff0c;可以选择适配自己电脑的版本&#xff0c;一般来说应该是Windows x64的。不要直接点W…

制作电商网站帮助中心,节省60%的咨询工作量

随着电子商务的快速发展&#xff0c;越来越多的企业选择在网上建立自己的电商平台。然而&#xff0c;一旦电商网站上线&#xff0c;就会面临一系列的问题和挑战。其中一个重要问题是如何有效管理和解答大量用户的咨询和问题&#xff0c;这对于提高用户体验和促进销售至关重要。…

Apache Doris IP变更问题详解

Apache Doris IP变更问题详解 一、背景二、环境硬件信息软件信息 三、FE恢复3.1 异常日志3.2 获取当前ip3.3 重置ip信息3.4 重置元数据记录3.5 元数据模式恢复3.6 重置fe集群节点3.7 关闭元数据模式重启fe 四、BE恢复4.1 获取当前ip4.2 重置ip信息4.3 重置be集群节点 一、背景 …

Java课题笔记~ Ajax

1.1 概述 AJAX (Asynchronous JavaScript And XML)&#xff1a;异步的 JavaScript 和 XML。 我们先来说概念中的 JavaScript 和 XML&#xff0c;JavaScript 表明该技术和前端相关&#xff1b;XML 是指以此进行数据交换。 1.1.1 作用 AJAX 作用有以下两方面&#xff1a; 与服…

矩形重叠问题

矩形重叠 文章目录 题目描述解题思路方法一方法二 题目描述 矩形以列表 [x1, y1, x2, y2] 的形式表示&#xff0c;其中 (x1, y1) 为左下角的坐标&#xff0c;(x2, y2) 是右上角的坐标。矩形的上下边平行于 x 轴&#xff0c;左右边平行于 y 轴。 如果相交的面积为 正 &#xff0…

中国电信秋招攻略,考试内容分析

电信秋招简介 每年的毕业生人数都在逐年递增&#xff0c;逐年递增就意味着竞争会越来越大&#xff0c;最好比别人做更充足的准备。要确定好就业方向以及就业的岗位&#xff0c;要了解各种各样的流程&#xff0c;做好一切自己能做到的准备。而对于有想法进入电信公司工作的人来…

PL 侧驱动和fpga 重加载的方法

可以解决很多的问题 时钟稳定后加载特定fpga ip &#xff08;要不内核崩的一塌糊涂&#xff09;fpga 稳定复位软件决定fpga ip 加载的时序 dluash load /usr/local/scripts/si5512_setup.lua usleep 30 mkdir -p /lib/firmware cp -rf /usr/local/firmare/{*.bit.bin,*.dtbo} …

Go语言入门指南:基础语法和常用特性解析(上)

一、Go语言前言 Go是一种静态类型的编译语言&#xff0c;常常被称作是21世纪的C语言。Go语言是一个开源项目&#xff0c;可以免费获取编译器、库、配套工具的源代码&#xff0c;也是高性能服务器和应用程序的热门选择。 Go语言可以运行在类UNIX系统——比如Linux、OpenBSD、M…

[RDMA] 高性能异步的消息传递和RPC :Accelio

1. Introduce Accelio是一个高性能异步的可靠消息传递和RPC库&#xff0c;能优化硬件加速。 RDMA和TCP / IP传输被实现&#xff0c;并且其他的传输也能被实现&#xff0c;如共享存储器可以利用这个高效和方便的API的优点。Accelio 是 Mellanox 公司的RDMA中间件&#xff0c;用…

k8s扩缩容与滚动更新

使用kubectl run创建应用 kubectl run kubernetes-bootcamp \> --imagedocker.io/jocatalin/kubernetes-bootcamp:v1 \> --port8080 端口暴露出去 kubectl expose pod kubernetes-bootcamp --type"NodePort" --port 8080 使用kubectl create创建应用 kubect…