深入理解python虛擬機(jī):程序執(zhí)行的載體——棧幀
棧幀(Stack Frame)是 Python 虛擬機(jī)中程序執(zhí)行的載體之一,也是 Python 中的一種執(zhí)行上下文。每當(dāng) Python 執(zhí)行一個(gè)函數(shù)或方法時(shí),都會(huì)創(chuàng)建一個(gè)棧幀來表示當(dāng)前的函數(shù)調(diào)用,并將其壓入一個(gè)稱為調(diào)用棧(Call Stack)的數(shù)據(jù)結(jié)構(gòu)中。調(diào)用棧是一個(gè)后進(jìn)先出(LIFO)的數(shù)據(jù)結(jié)構(gòu),用于管理程序中的函數(shù)調(diào)用關(guān)系。
棧幀的創(chuàng)建和銷毀是動(dòng)態(tài)的,隨著函數(shù)的調(diào)用和返回而不斷發(fā)生。當(dāng)一個(gè)函數(shù)被調(diào)用時(shí),一個(gè)新的棧幀會(huì)被創(chuàng)建并推入調(diào)用棧,當(dāng)函數(shù)調(diào)用結(jié)束后,對應(yīng)的棧幀會(huì)從調(diào)用棧中彈出并銷毀。
棧幀的使用使得 Python 能夠?qū)崿F(xiàn)函數(shù)的嵌套調(diào)用和遞歸調(diào)用。通過不斷地創(chuàng)建和銷毀棧幀,Python 能夠跟蹤函數(shù)調(diào)用關(guān)系,保存和恢復(fù)局部變量的值,實(shí)現(xiàn)函數(shù)的嵌套和遞歸執(zhí)行。同時(shí),棧幀還可以用于實(shí)現(xiàn)異常處理、調(diào)試信息的收集和優(yōu)化技術(shù)等。
需要注意的是,棧幀是有限制的,Python 解釋器會(huì)對棧幀的數(shù)量和大小進(jìn)行限制,以防止棧溢出和資源耗盡的情況發(fā)生。在編寫 Python 程序時(shí),合理使用函數(shù)調(diào)用和棧幀可以幫助提高程序的性能和可維護(hù)性。
棧幀數(shù)據(jù)結(jié)構(gòu)
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;
內(nèi)存申請和棧幀的內(nèi)存布局
在 cpython 當(dāng)中,當(dāng)我們需要申請一個(gè) frame object 對象的時(shí)候,首先需要申請內(nèi)存空間,但是在申請內(nèi)存空間的時(shí)候并不是單單申請一個(gè) frameobject 大小的內(nèi)存,而是會(huì)申請額外的內(nèi)存空間,大致布局如下所示。
- f_localsplus,這是一個(gè)數(shù)組用戶保存函數(shù)執(zhí)行的 local 變量,這樣可以直接通過下標(biāo)得到對應(yīng)的變量的值。
- ncells 和 nfrees,這個(gè)變量和我們前面在分析 code object 的函數(shù)閉包相關(guān),ncells 和 ncells 分別表示 cellvars 和 freevars 中變量的個(gè)數(shù)。
- stack,這個(gè)變量就是函數(shù)執(zhí)行的時(shí)候函數(shù)的棧幀,這個(gè)大小在編譯期間就可以確定因此可以直接確定??臻g的大小。
下面是在申請 frame object 的核心代碼:
Py_ssize_t extras, ncells, nfrees;
ncells = PyTuple_GET_SIZE(code->co_cellvars); // 得到 co_cellvars 當(dāng)中元素的個(gè)數(shù) 沒有的話則是 0
nfrees = PyTuple_GET_SIZE(code->co_freevars); // 得到 co_freevars 當(dāng)中元素的個(gè)數(shù) 沒有的話則是 0
// extras 就是表示除了申請 frame object 自己的內(nèi)存之后還需要額外申請多少個(gè) 指針對象
// 確切的帶來說是用于保存 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;
}
}
// 這個(gè)就是函數(shù)的 code object 對象 將其保存到棧幀當(dāng)中 f 就是棧幀對象
f->f_code = code;
extras = code->co_nlocals + ncells + nfrees;
// 這個(gè)就是棧頂?shù)奈恢?注意這里加上的 extras 并不包含棧的大小
f->f_valuestack = f->f_localsplus + extras;
// 對額外申請的內(nèi)存空間盡心初始化操作
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; // 將棧頂?shù)闹羔樦赶驐5钠鹗嘉恢? 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;
現(xiàn)在我們對 frame object 對象當(dāng)中的各個(gè)字段進(jìn)行分析,說明他們的作用:
- PyObject_VAR_HEAD:表示對象的頭部信息,包括引用計(jì)數(shù)和類型信息。
- f_back:前一個(gè)棧幀對象的指針,或者為NULL。
- f_code:指向 PyCodeObject 對象的指針,表示當(dāng)前幀執(zhí)行的代碼段。
- f_builtins:指向 PyDictObject 對象的指針,表示當(dāng)前幀的內(nèi)置符號表,字典對象,鍵是字符串,值是對應(yīng)的 python 對象。
- f_globals:指向 PyDictObject 對象的指針,表示當(dāng)前幀的全局符號表。
- f_locals:指向任意映射對象的指針,表示當(dāng)前幀的局部符號表。
- f_valuestack:指向當(dāng)前幀的值棧底部的指針。
- f_stacktop:指向當(dāng)前幀的值棧頂部的指針。
- f_trace:指向跟蹤函數(shù)對象的指針,用于調(diào)試和追蹤代碼執(zhí)行過程,這個(gè)字段我們在后面的文章當(dāng)中再進(jìn)行分析。
- f_exc_type、f_exc_value、f_exc_traceback:這個(gè)字段和異常相關(guān),在函數(shù)執(zhí)行的時(shí)候可能會(huì)產(chǎn)生錯(cuò)誤異常,這個(gè)就是用于處理異常相關(guān)的字段。
- f_gen:指向當(dāng)前生成器對象的指針,如果當(dāng)前幀不是生成器,則為NULL。
- f_lasti:上一條指令在字節(jié)碼當(dāng)中的下標(biāo)。
- f_lineno:當(dāng)前執(zhí)行的代碼行號。
- f_iblock:當(dāng)前執(zhí)行的代碼塊在f_blockstack中的索引,這個(gè)字段也主要和異常的處理有關(guān)系。
- f_executing:表示當(dāng)前幀是否仍在執(zhí)行。
- f_blockstack:用于try和loop代碼塊的堆棧,最多可以嵌套 CO_MAXBLOCKS 層。
- f_localsplus:局部變量和值棧的組合,是一個(gè)動(dòng)態(tài)大小的數(shù)組。
如果我們在一個(gè)函數(shù)當(dāng)中調(diào)用另外一個(gè)函數(shù),這個(gè)函數(shù)再調(diào)用其他函數(shù)就會(huì)形成函數(shù)的調(diào)用鏈,就會(huì)形成下圖所示的鏈?zhǔn)浇Y(jié)構(gòu)。
例子分析
我們現(xiàn)在來模擬一下下面的函數(shù)的執(zhí)行過程。
import dis
def foo():
a = 1
b = 2
return a + b
if __name__ == '__main__':
dis.dis(foo)
print(foo.__code__.co_stacksize)
foo()
上面的 foo 函數(shù)的字節(jié)碼如下所示:
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
函數(shù) foo 的 stacksize 等于 2 。
初始時(shí) frameobject 的布局如下所示:
現(xiàn)在執(zhí)行第一條指令 LOAD_CONST 此時(shí)的 f_lasti 等于 -1,執(zhí)行完這條字節(jié)碼之后棧幀情況如下:
在執(zhí)行完這條字節(jié)碼之后 f_lasti 的值變成 0。字節(jié)碼 LOAD_CONST 對應(yīng)的 c 源代碼如下所示:
TARGET(LOAD_CONST) {
PyObject *value = GETITEM(consts, oparg); // 從常量表當(dāng)中取出下標(biāo)為 oparg 的對象
Py_INCREF(value);
PUSH(value);
FAST_DISPATCH();
}
首先是從 consts 將對應(yīng)的常量拿出來,然后壓入棧空間當(dāng)中。
再執(zhí)行 STORE_FAST 指令,這個(gè)指令就是將棧頂?shù)脑貜棾鋈缓蟊4娴角懊嫣岬降?f_localsplus 數(shù)組當(dāng)中去,那么現(xiàn)在??臻g是空的。STORE_FAST 對應(yīng)的 c 源代碼如下:
TARGET(STORE_FAST) {
PyObject *value = POP(); // 將棧頂元素彈出
SETLOCAL(oparg, value); // 保存到 f_localsplus 數(shù)組當(dāng)中去
FAST_DISPATCH();
}
執(zhí)行完這條指令之后 f_lasti 的值變成 2 。
接下來的兩條指令和上面的一樣,就不做分析了,在執(zhí)行完兩條指令,f_lasti 變成 6 。
接下來兩條指令分別將 a b 加載進(jìn)入??臻g單中現(xiàn)在棧空間布局如下所示:
然后執(zhí)行 BINARY_ADD 指令 彈出??臻g的兩個(gè)元素并且把他們進(jìn)行相加操作,最后將得到的結(jié)果再壓回??臻g當(dāng)中。
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); // 將結(jié)果壓入棧中
if (sum == NULL)
goto error;
DISPATCH();
}
最后執(zhí)行 RETURN_VALUE 指令將??臻g結(jié)果返回。
總結(jié)
在本篇文章當(dāng)中主要介紹了 cpython 當(dāng)中的函數(shù)執(zhí)行的時(shí)候的棧幀結(jié)構(gòu),這里面包含的程序執(zhí)行時(shí)候所需要的一些必要的變量,比如說全局變量,python 內(nèi)置的一些對象等等,同時(shí)需要注意的是 python 在查詢對象的時(shí)候如果本地 f_locals 沒有找到就會(huì)去全局 f_globals 找,如果還沒有找到就會(huì)去 f_builtins 里面的找,當(dāng)一個(gè)程序返回的時(shí)候就會(huì)找到 f_back 他上一個(gè)執(zhí)行的棧幀,將其設(shè)置成當(dāng)前線程正在使用的棧幀,這就完成了函數(shù)的調(diào)用返回,關(guān)于這個(gè)棧幀還有一些其他的字段我們沒有談到在后續(xù)的文章當(dāng)中將繼續(xù)深入其中一些字段。
本篇文章是深入理解 python 虛擬機(jī)系列文章之一,文章地址:https://github.com/Chang-LeHung/dive-into-cpython
更多精彩內(nèi)容合集可訪問項(xiàng)目:https://github.com/Chang-LeHung/CSCore文章來源:http://www.zghlxwxcb.cn/news/detail-424209.html
關(guān)注公眾號:一無是處的研究僧,了解更多計(jì)算機(jī)(Java、Python、計(jì)算機(jī)系統(tǒng)基礎(chǔ)、算法與數(shù)據(jù)結(jié)構(gòu))知識。文章來源地址http://www.zghlxwxcb.cn/news/detail-424209.html
到了這里,關(guān)于深入理解python虛擬機(jī):程序執(zhí)行的載體——棧幀的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!