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

14次阅读

共计 6151 个字符,预计需要花费 16 分钟才能阅读完成。

深刻了解 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 他上一个执行的栈帧,将其设置成以后线程正在应用的栈帧,这就实现了函数的调用返回,对于这个栈帧还有一些其余的字段咱们没有谈到在后续的文章当中将持续深刻其中一些字段。


本篇文章是深刻了解 python 虚拟机系列文章之一,文章地址:https://github.com/Chang-LeHung/dive-into-cpython

更多精彩内容合集可拜访我的项目:https://github.com/Chang-LeHung/CSCore

关注公众号:一无是处的钻研僧,理解更多计算机(Java、Python、计算机系统根底、算法与数据结构)常识。

正文完
 0