一、 什么是堆栈?
栈帧也叫过程活动记录 ,是编译器 用来实现过程/函数调用 的一种数据结构。—–百度百科
栈帧对象其中所使用的数据结构是我们常见的栈,栈是一种受限的线性表,具备后进先出的特性。栈这个数据结构具有记忆效果,常见的使用场景比如浏览器的前进和后退。在几乎所有语言中,栈帧都是实现函数调用功能的标配,栈帧中可以保存某一函数运行过程中的上下文信息,即过程活动记录。今天,通过阅读源码一起探究,在Python中,是如何使用栈帧对象(栈结构)来实现函数的调用和相关上下文信息的保存和获取的, 以及分析Python VM执行字节码的原理。
二、 栈帧对象的结构体 搜索Python实现的源码,发现栈帧结构有两部分,一部分是外部栈帧(PyFrameObject),另外一部分是内部栈帧(InterpreterFrame)。先来看第一部分。
1.PyFrameObject对象位于/Include/pyframe.h头文件中:
1 2 3 4 5 6 7 8 9 10 struct _frame { PyObject_HEAD struct _frame *f_back ; struct _interpreter_frame *f_frame ; PyObject *f_trace; int f_lineno; char f_trace_lines; char f_trace_opcodes; char f_own_locals_memory; };
2.InterpreterFrame对象位于/Include/internal/pycore_frame.h头文件中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 typedef struct _interpreter_frame { PyFunctionObject *f_func; PyObject *f_globals; PyObject *f_builtins; PyObject *f_locals; PyCodeObject *f_code; PyFrameObject *frame_obj; PyObject *generator; struct _interpreter_frame *previous ; int f_lasti; int stacktop; PyFrameState f_state; PyObject *localsplus[1 ]; } InterpreterFrame;
看完了上述两个数据结构,栈帧对象是通过双向链表的形式组织起来,我大致绘了如下关于栈帧对象的数据结构图。
通过上述图,我们可以很清楚的看出来栈帧对象是以双向链表的形式组合在一起。那么这个栈帧对象链又是被谁管理的呢?我们进一步分析。
在Include/internal/pycore_frame.h中,我们可以找到对链栈的一系列的实现,包含判断栈帧的状态、获取栈帧的基地址、获取栈帧的栈顶对象、出栈、入栈,获取栈顶的栈顶地址,获取栈帧的局部变量(局部变量以数组形式存储,在编译时就可确定个数,方便通过偏移量直接定位)等等。首先来看栈帧的初始化函数_PyFrame_InitializeSpecials
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 static inline void _PyFrame_InitializeSpecials( InterpreterFrame *frame, PyFunctionObject *func, PyObject *locals, int nlocalsplus) { Py_INCREF(func); frame->f_func = func; frame->f_code = (PyCodeObject *)Py_NewRef(func->func_code); frame->f_builtins = func->func_builtins; frame->f_globals = func->func_globals; frame->f_locals = Py_XNewRef(locals); frame->stacktop = nlocalsplus; frame->frame_obj = NULL ; frame->generator = NULL ; frame->f_lasti = -1 ; frame->f_state = FRAME_CREATED; frame->is_entry = false ; }
继续顺藤摸瓜,看哪些地方调用它。在Python/ceval.c文件中的_PyEvalFramePushAndInit中,ceval.c文件中基本都是与Python VM执行字节码相关的程序有关。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 static InterpreterFrame * _PyEvalFramePushAndInit(PyThreadState *tstate, PyFunctionObject *func, PyObject *locals, PyObject* const * args, size_t argcount, PyObject *kwnames) { PyCodeObject * code = (PyCodeObject *)func->func_code; size_t size = code->co_nlocalsplus + code->co_stacksize + FRAME_SPECIALS_SIZE; InterpreterFrame *frame = _PyThreadState_BumpFramePointer(tstate, size); if (frame == NULL ) { goto fail; } _PyFrame_InitializeSpecials(frame, func, locals, code->co_nlocalsplus); PyObject **localsarray = &frame->localsplus[0 ]; for (int i = 0 ; i < code->co_nlocalsplus; i++) { localsarray[i] = NULL ; } if (initialize_locals(tstate, func, localsarray, args, argcount, kwnames)) { _PyFrame_Clear(frame); return NULL ; } return frame; fail: for (size_t i = 0 ; i < argcount; i++) { Py_DECREF(args[i]); } if (kwnames) { Py_ssize_t kwcount = PyTuple_GET_SIZE(kwnames); for (Py_ssize_t i = 0 ; i < kwcount; i++) { Py_DECREF(args[i+argcount]); } } PyErr_NoMemory(); return NULL ; }
说明:
1._PyEvalFramePushAndInit
该函数会用于call_function等字节码中,因为调用函数,意味着创建新的栈帧对象,并加入到栈帧调用链中,进入子函数后,栈帧也相应的跳到对应的栈帧对象,然后执行栈帧对象中的代码对象,执行完成,将返回结果保存到调用方的栈帧对象中并释放回收当前栈帧。
2.该函数中有两个比较重要的函数,分别是_PyThreadState_BumpFramePointer
, initialize_locals
两个函数,前者时在本地线程的上下文中申请size大小的栈帧对象,并添加到栈帧链的末尾(逻辑结构上认为是链式)。后者时初始化栈帧对象中的函数局部变量,包含函数的参数等。接下来就来分析下这两个函数内部的实现。
3._PyThreadState_BumpFramePointer
函数源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 static inline InterpreterFrame * _PyThreadState_BumpFramePointer(PyThreadState *tstate, size_t size) { PyObject **base = tstate->datastack_top; if (base) { PyObject **top = base + size; assert(tstate->datastack_limit); if (top < tstate->datastack_limit) { tstate->datastack_top = top; return (InterpreterFrame *)base; } } return _PyThreadState_BumpFramePointerSlow(tstate, size); }struct _ts { ... PyObject **datastack_top; PyObject **datastack_limit; ...
4.initialize_locals
函数源码如下:
5.看了上述分析,做一个小的总结:
栈帧对象在存储结构上是以连续的动态数组的方式组织起来,逻辑结构上,采用双向链表的方式组织起来。使用数组的好处是可以通过偏移量直接定位数据;使用双向链表的好处是插入(分配),删除(回收)更加高效。所以阅读源码,不仅仅只是阅读,而是学着理解设计者设计和实现的思路并丰富自己。
三、Python 执行过程及其VM的执行原理 众所周知,Python是解释型语言,从Python应用层面来看,Python是允许写一部分代码,就执行一部分代码,使用过jupyter notebook的同学应该都深有体会。
1.一般,所有语言的入口函数基本上都以main函数开头,Python也不例外。Python执行py文件的入口源码位于/Python/pythonrun.c
1).执行Python某py文件时,首先会打开py文件并封装成文件对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 PyObject * PyRun_FileExFlags(FILE *fp, const char *filename, int start, PyObject *globals, PyObject *locals, int closeit, PyCompilerFlags *flags) { PyObject *filename_obj = PyUnicode_DecodeFSDefault(filename); if (filename_obj == NULL ) { return NULL ; } PyObject *res = pyrun_file(fp, filename_obj, start, globals, locals, closeit, flags); Py_DECREF(filename_obj); return res; }
2).根据文件对象,构建AST抽象语法数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 static PyObject * pyrun_file(FILE *fp, PyObject *filename, int start, PyObject *globals, PyObject *locals, int closeit, PyCompilerFlags *flags) { mod_ty mod; mod = _PyParser_ASTFromFile(fp, filename, NULL , start, NULL , NULL , flags, NULL , arena); if (closeit) { fclose(fp); } PyObject *ret; if (mod != NULL ) { ret = run_mod(mod, filename, globals, locals, flags, arena); } else { ret = NULL ; } _PyArena_Free(arena); return ret; }
3).将AST抽象语法数转为字节码,并执行字节码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static PyObject * run_mod(mod_ty mod, PyObject *filename, PyObject *globals, PyObject *locals, PyCompilerFlags *flags, PyArena *arena) { PyThreadState *tstate = _PyThreadState_GET(); PyCodeObject *co = _PyAST_Compile(mod, filename, flags, -1 , arena); if (co == NULL ) return NULL ; if (_PySys_Audit(tstate, "exec" , "O" , co) < 0 ) { Py_DECREF(co); return NULL ; } PyObject *v = run_eval_code_obj(tstate, co, globals, locals); Py_DECREF(co); return v; }
总结:
Python执行某py文件的过程大体上可以分为三部:
2.在了解了Python执行py文件的过程后,可以发现Python VM真正执行的是一行一行的字节码。Python自己定义了一套字节码,
位于/Include/opcode.h头文件中,在我目前的Python3.11.0 a0版本中,已经增长到155个字节码了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 def test (name, age, *args ): print (name, age)print (dis(test)) 56 0 LOAD_GLOBAL 0 (print ) 2 LOAD_FAST 0 (name) 4 LOAD_FAST 1 (age) 6 CALL_FUNCTION 2 8 POP_TOP 10 LOAD_CONST 0 (None ) 12 RETURN_VALUENone
3.接下来,分析下_PyEval_EvalFrameDefault
函数,它是Python VM 执行字节码的核心函数,并结合_PyEval_EvalFrameDefault
函数,分析下上述函数对应的各个字节码的执行逻辑。_PyEval_EvalFrameDefault
函数位于/Python/ceval.c文件中。由于该函数代码段过长,我这里截取部分进行分析,其他部分用...
代替。
首先了解下字节码的结构组成,总共16bit, 根据自己主机的CPU端模式的架构决定,我的电脑上是小端存储。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #ifdef WORDS_BIGENDIAN # define _Py_OPCODE(word) ((word) >> 8) # define _Py_OPARG(word) ((word) & 255) # define _Py_MAKECODEUNIT(opcode, oparg) (((opcode)<<8)|(oparg)) #else # define _Py_OPCODE(word) ((word) & 255) # define _Py_OPARG(word) ((word) >> 8) # define _Py_MAKECODEUNIT(opcode, oparg) ((opcode)|((oparg)<<8)) #endif
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 PyObject* _Py_HOT_FUNCTION_PyEval_EvalFrameDefault(PyThreadState * tstate , InterpreterFrame * frame , int throwflag ) { int opcode; int oparg; PyObject *retval = NULL; PyCodeObject *co = frame->f_code; PyObject *names = co->co_names; PyObject *consts = co->co_consts; PyObject **stack_pointer = _PyFrame_GetStackPointer(frame ) ; frame->stacktop = -1 ; frame->f_state = FRAME_EXECUTING; opcode = _Py_OPCODE(* next_instr ) ; dispatch_opcode: switch (opcode) { TARGET(NOP) : { DISPATCH() ; } TARGET(LOAD_CLOSURE) : TARGET(LOAD_FAST) : { PyObject *value = GETLOCAL(oparg ) ; if (value == NULL) { goto unbound_local_error; } Py_INCREF(value ) ; PUSH(value ) ; DISPATCH() ; } TARGET(LOAD_CONST) : { PREDICTED(LOAD_CONST) ; PyObject *value = GETITEM(consts , oparg ) ; Py_INCREF(value ) ; PUSH(value ) ; DISPATCH() ; } TARGET(STORE_FAST) : { PREDICTED(STORE_FAST) ; PyObject *value = POP() ; SETLOCAL(oparg , value ) ; DISPATCH() ; } TARGET(CALL_METHOD) : { PyObject **sp, *res; int meth_found; sp = stack_pointer; meth_found = (PEEK(oparg + 2) != NULL); res = call_function(tstate , &sp , oparg + meth_found , NULL, cframe .use_tracing ) ; stack_pointer = sp; STACK_SHRINK(1 - meth_found ) ; PUSH(res ) ; if (res == NULL) { goto error; } CHECK_EVAL_BREAKER() ; DISPATCH() ; } TARGET(CALL_FUNCTION) : { PREDICTED(CALL_FUNCTION) ; PyObject **sp, *res; sp = stack_pointer; res = call_function(tstate , &sp , oparg , NULL, cframe .use_tracing ) ; stack_pointer = sp; PUSH(res ) ; if (res == NULL) { goto error; } CHECK_EVAL_BREAKER() ; DISPATCH() ; }
说明: