0x01 Python虛擬機中的執(zhí)行環(huán)境
Python
虛擬機在執(zhí)行Python
代碼時,是模擬操作系統(tǒng)執(zhí)行可執(zhí)行文件的過程。
ESP
(Extended stack pointer
)為棧指針席噩,用于指向棧的棧頂(下一個壓入棧的活動記錄的頂部)月匣,而EBP
(extended base pointer
)為幀指針承边,指向當前活動記錄的底部
對于一個函數(shù)而言桌肴,其所有對局部變量的操作都在自己的棧幀中完成,而函數(shù)之間的調(diào)用則通過創(chuàng)建新的棧幀完成色鸳。
Python
源代碼編譯完成后社痛,所有的字節(jié)碼指令和靜態(tài)信息都在PyCodeObject
對象中,但是只有這些還是不夠的命雀,還需要執(zhí)行環(huán)境蒜哀。
執(zhí)行環(huán)境(PyFrameObject)
當
Python
執(zhí)行test.py
的第一條表達式時,會創(chuàng)建一個執(zhí)行環(huán)境A
(PyFrameObject *A
)吏砂,所有的字節(jié)碼都在這個執(zhí)行環(huán)境A
中執(zhí)行撵儿,Python
可以從這個執(zhí)行環(huán)境中獲取變量的值,也可以根據(jù)字節(jié)碼指令修改執(zhí)行環(huán)境中變量的值狐血,以影響后續(xù)的字節(jié)碼指令淀歇;這樣的過程會一直持續(xù)下去,直到發(fā)生函數(shù)的調(diào)用匈织,執(zhí)行函數(shù)調(diào)用的字節(jié)碼時浪默,會在當前執(zhí)行環(huán)境
A
之外創(chuàng)建一個新的執(zhí)行環(huán)境B
(PyFrameObject *B
)。這塊的執(zhí)行環(huán)境對應運行時棧中的棧幀缀匕。
typedef struct _frame {
PyObject_VAR_HEAD
struct _frame *f_back; /* 執(zhí)行環(huán)境鏈上的前一個frame previous frame, or NULL */
PyCodeObject *f_code; /* PyCodeObject對象 code segment */
PyObject *f_builtins; /* builtin名字空間 builtin symbol table (PyDictObject) */
PyObject *f_globals; /* global名字空間 global symbol table (PyDictObject) */
PyObject *f_locals; /* local名字空間 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 */
/* If an exception is raised in this frame, the next three are used to
* record the exception info (if any) originally in the thread state. See
* comments before set_exc_info() -- it's not obvious.
* Invariant: if _type is NULL, then so are _value and _traceback.
* Desired invariant: all three are NULL, or all three are non-NULL. That
* one isn't currently true, but "should be".
*/
PyObject *f_exc_type, *f_exc_value, *f_exc_traceback;
PyThreadState *f_tstate;
int f_lasti; /* 上一條字節(jié)碼指令在f_code中的偏移位置 Last instruction if called */
/* As of 2.3 f_lineno is only valid when tracing is active (i.e. when
f_trace is set) -- at other times use PyCode_Addr2Line instead. */
int f_lineno; /* 當前字節(jié)碼對應的源代碼行 Current line number */
int f_iblock; /* index in f_blockstack */
PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
// 動態(tài)內(nèi)存纳决,維護(局部變量+cell對象集合+free對象集合+運行時棧)所需的空間
PyObject *f_localsplus[1]; /* locals+stack, dynamically sized */
} PyFrameObject;
-
f_back
指向上一個棧幀,用這個域來模擬操作系統(tǒng)中棧幀的關系(操作系統(tǒng)使用ebp
和esp
來維護棧幀關系) -
f_code
中存放的是待執(zhí)行的PyCodeObject
對象 -
f_builtins
乡小、f_globals
阔加、f_locals
是3
個獨立的名字空間(這里可以明確執(zhí)行環(huán)境和名字空間的關系) -
PyObject_VAR_HEAD
表示PyFrameObject
是一個變長的對象,類似于PyStringObject
一樣满钟,每個新對象的大小可能不一樣胜榔。那變長的內(nèi)存是用來干啥的呢?- 每一個
PyFrameObject
對象都維護了一個PyCodeObject
對象 - 在將
.py
代碼編譯成PyCodeObject
對象的時候湃番,會計算這段Code Block
執(zhí)行過程中所需的椮仓空間大小,存儲在PyCodeObject.co_stacksize
域牵辣,不同的Code Block
所需的椝ぱⅲ空間不同,這個就是變長的內(nèi)存的用處
- 每一個
-
Python
在執(zhí)行計算的時候也需要一些內(nèi)存空間(存儲臨時變量等內(nèi)容)纬向,我們將其稱為“運行時椩褡牵”
PyFrameObject中的動態(tài)內(nèi)存空間
// frameobject.c 有刪減
PyFrameObject *
PyFrame_New(PyThreadState *tstate, PyCodeObject *code, PyObject *globals,
PyObject *locals)
{
// 從PyThreadState對象中獲取得到當前線程的執(zhí)行環(huán)境
PyFrameObject *back = tstate->frame;
PyFrameObject *f;
PyObject *builtins;
Py_ssize_t i;
Py_ssize_t extras, ncells, nfrees;
ncells = PyTuple_GET_SIZE(code->co_cellvars);
nfrees = PyTuple_GET_SIZE(code->co_freevars);
// 4部分構成了PyFrameObject維護的動態(tài)內(nèi)存區(qū)
extras = code->co_stacksize + code->co_nlocals + ncells + nfrees;
// 創(chuàng)建新的執(zhí)行環(huán)境
f = PyObject_GC_NewVar(PyFrameObject, &PyFrame_Type, extras);
// 計算初始化時“運行時棧”的棧頂
extras = code->co_nlocals + ncells + nfrees;
// f_valuestack 維護“運行時椨馓酰”的棧底琢岩,f_stacktop 維護“運行時棧”的棧頂
f->f_valuestack = f->f_localsplus + extras;
f->f_stacktop = f->f_valuestack;
// 鏈接當前執(zhí)行環(huán)境
f->f_back = back;
f->f_tstate = tstate;
return f;
}
-
PyFrameObject.f_localsplus
域包含了PyCodeObject
對象中存儲的局部變量(co_nlocals
)师脂、co_freevars
担孔、co_cellvars
和運行時棧。 -
f_valuestack
指向棧底吃警,f_stacktop
指向棧頂糕篇。
0x02 名字、作用域和名字空間
名字:就是一個符號酌心,用于代表某些事物的一個有助于記憶的字符序列拌消。名字最終的作用并不在于名字本身,而在于名字所代表的事物安券。
作用域:
Python
是具有靜態(tài)作用域(也稱詞法作用域)的墩崩,即作用域由源程序的文本決定的,一個Code Block
就是一個作用域侯勉,在寫Python
代碼的時候鹦筹,作用域就已經(jīng)確定了名字空間:名字空間是與作用域?qū)膭討B(tài)的東西,上面提到的
f_builtins
址貌、f_globals
铐拐、f_locals
3
個獨立的名字空間
約束與名字空間
賦值語句(具有賦值行為的語句,import xxx
练对、class A(object):
都算)是一類特殊的語句余舶,因為它會影響名字空間。
賦值語句被執(zhí)行了以后锹淌,會得到(name匿值,obj)
這樣的關聯(lián)關系,稱之為約束赂摆。賦值語句就是建立約束的地方挟憔,約束的容身之處就是名字空間。
在Python
中名字空間用PyDictObject
對象表示烟号,約束剛好與鍵值對對應起來绊谭。
一個對象的名字空間中所有的名字都稱為對象的屬性。這樣看汪拥,賦值語句也具有“設置對象屬性的行為”达传,那“訪問對象屬性”的動作稱為屬性引用,屬性引用就是使用另一個名字空間中的名字(eg:import A
&print A.a
)
作用域與名字空間
一個module
對應一個名字空間,module
內(nèi)部可能有多個名字空間宪赶,每一個名字空間與一個作用域?qū)?/p>
一個約束起作用的那一段程序正文區(qū)域稱為這個約束的作用域宗弯。一個作用域則是指一段程序正文區(qū)域,一旦出了這個正文區(qū)域搂妻,這個約束就不起作用了蒙保。
位于一個作用域中的代碼可以直接訪問作用域中出現(xiàn)的名字,稱為“直接訪問”(直接print a
不需要print A.a
)欲主。就是指不用加上屬性引用方式的訪問修飾符“.
”邓厕。訪問名字這樣的行為被稱為“名字引用”。
Python
的名字空間的行為被它所支持的嵌套作用域影響扁瓢,產(chǎn)生了最內(nèi)嵌套作用域規(guī)則(LEGB
):由一個賦值語句引進的名字在這個賦值語句所在的作用域里是可見(起作用)的详恼,而且在其內(nèi)部嵌套的每一個作用域里也是可見的,除非嵌套的作用域內(nèi)引几,被引進了同樣名字所遮蔽昧互。
LGB
在Python
中,一個module
對應的源文件定義了一個作用域她紫,稱為global
作用域(對應global
名字空間)硅堆;
一個函數(shù)定義了一個local
作用域(對應local
名字空間);
Python
自身還定義了一個最頂層作用域贿讹,builtin
作用域(對應builtin
名字空間)渐逃;
Python 2.2
之前這3
個作用域就已經(jīng)存在,被稱為LGB
規(guī)則:名字引用動作沿著local
作用域民褂、global
作用域茄菊、builtin
作用域的順序查找名字的對應約束。
LEGB
Python 2.2
開始赊堪,Python
引入嵌套函數(shù)面殖,這時候又加了一層enclosing
作用域,稱為LEGB
哭廉。
嵌套函數(shù)會將在直接外圍作用域中使用到的約束脊僚,與嵌套函數(shù)捆綁在一起,捆綁起來的整體被稱為“閉包”遵绰。
global表達式
當一個作用域中出現(xiàn)global
語句時辽幌,就意味著我們強制命令Python
對某個名字的引用只參考global
名字空間,而不用去管LEGB
規(guī)則椿访。
屬性引用與名字引用
屬性引用實質(zhì)上也是一種名字引用乌企,其本質(zhì)就是到名字空間中去查找一個名字所引用的對象。但是屬性引用可以視為一種特殊的名字引用成玫,它不受LEGB
規(guī)則制約加酵。
屬性引用沒有嵌套作用域拳喻,在名字空間中查找名字時,有就有猪腕,沒有就沒有冗澈,沒有更多的規(guī)則限制。
名字引用遵循的LEGB
規(guī)則不會越過module
的邊界码撰。
0x03 Python虛擬機的運行框架(偽CPU)
當Python
啟動后渗柿,首先會進行Python
運行時環(huán)境的初始化个盆。這里的運行時環(huán)境和上面提到的執(zhí)行環(huán)境是不同的概念脖岛。運行時環(huán)境是一個全局的概念,而執(zhí)行環(huán)境實際上就是一個棧幀(PYFrameObject
)颊亮,是一個和某一個Code Block
對應的概念柴梆。
初始化完成以后,就開始運行程序终惑,入口就是PyEval_EvalFramEx()
绍在,這個函數(shù)就是Python
虛擬機的具體實現(xiàn)。
PyEval_EvalFramEx()
函數(shù)代碼有2000
多行雹有,就不列出所有代碼了偿渡。
- 首先會初始化一些變量;然后初始化了堆棧的棧頂指針霸奕,使其指向
f->f_stacktop
-
PyCodeObject
對象的co_code
域中保存著字節(jié)碼指令和字節(jié)碼指令的參數(shù)溜宽,其實它就是C
中的普通字符數(shù)組,使用3
個變量來遍歷整個字節(jié)碼字符數(shù)組:first_instr
永遠指向字節(jié)碼指令序列的開始位置质帅,next_instr
永遠指向下一條待執(zhí)行的字節(jié)碼指令位置适揉,f_lasti
指向上一條已經(jīng)執(zhí)行過的字節(jié)碼指令位置。 - 執(zhí)行字節(jié)碼指令的動作是如何實現(xiàn)的呢煤惩?
-
Python
虛擬機執(zhí)行字節(jié)碼指令的整體框架:其實就是一個for
循環(huán)加上一個巨大的switch/case
結(jié)構嫉嘀。
-
PyObject *
PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
{
......
// 獲取當前活動線程的線程狀態(tài)對象(PyThreadState)
PyThreadState *tstate = PyThreadState_GET();
// 設置線程狀態(tài)對象中的frame
tstate->frame = f;
co = f->f_code;
names = co->co_names;
consts = co->co_consts;
why = WHY_NOT;
......
for (;;) {
fast_next_opcode:
f->f_lasti = INSTR_OFFSET();
// 獲取字節(jié)碼指令
opcode = NEXTOP();
oparg = 0;
// 如果指令有參數(shù),獲取參數(shù)
if (HAS_ARG(opcode))
oparg = NEXTARG();
dispatch_opcode:
......
}
}
在這個執(zhí)行框架中魄揉,對字節(jié)碼的一步一步的遍歷是通過下面幾個宏來實現(xiàn)的:
// ceval.c
/* Code access macros */
#define INSTR_OFFSET() ((int)(next_instr - first_instr))
#define NEXTOP() (*next_instr++)
#define NEXTARG() (next_instr += 2, (next_instr[-1]<<8) + next_instr[-2])
#define PEEKARG() ((next_instr[2]<<8) + next_instr[1])
#define JUMPTO(x) (next_instr = first_instr + (x))
#define JUMPBY(x) (next_instr += (x))
-
Python
的字節(jié)碼有的是帶參數(shù)的剪侮,有的是沒有參數(shù)的(通過HAS_ARG
宏來判斷),所以next_instr
的位移可能是不同的洛退,但是無論如何瓣俯,next_instr
總是指向Python
的下一條要執(zhí)行的字節(jié)碼。 - 然后
Python
在獲得了一條字節(jié)碼指令和其所需的指令參數(shù)后不狮,會對字節(jié)碼利用switch
進行判斷降铸,根據(jù)不同的指令來執(zhí)行不同的case
語句,case
語句就是Python
對字節(jié)碼指令的具體實現(xiàn)摇零。 - 在成功執(zhí)行完一條字節(jié)碼指令后推掸,
Python
的執(zhí)行流程會跳轉(zhuǎn)到fast_next_opcode
或者for
循環(huán)處,不管如何,接下來的動作都是獲取下一條字節(jié)碼指令和指令參數(shù)谅畅,然后執(zhí)行指令對應的case
語句登渣。 - 如此一條一條的遍歷
co_code
中包含的所有字節(jié)碼指令,最終完成了對Python
程序的執(zhí)行毡泻。
why
變量:它指示了在退出這個巨大的for
循環(huán)時Python
執(zhí)行引擎的狀態(tài)胜茧。
在執(zhí)行字節(jié)碼過程中可能會報錯或出現(xiàn)異常(exception
),在退出的時候我們需要知道原因仇味,why
就扮演者這個角色呻顽。
/* Status code for main loop (reason for stack unwind) */
enum why_code {
WHY_NOT = 0x0001, /* No error */
WHY_EXCEPTION = 0x0002, /* Exception occurred */
WHY_RERAISE = 0x0004, /* Exception re-raised by 'finally' */
WHY_RETURN = 0x0008, /* 'return' statement */
WHY_BREAK = 0x0010, /* 'break' statement */
WHY_CONTINUE = 0x0020, /* 'continue' statement */
WHY_YIELD = 0x0040 /* 'yield' operator */
};
0x04 Python運行時環(huán)境初探
進程(
process
)不是與機器指令序列相對應的活動對象,這個與可執(zhí)行文件中機器指令序列對應的活動對象是線程(Thread
)丹墨,而進程是線程的活動對象講人話就是說廊遍,在
CPU
上執(zhí)行任務的不是進程而是線程。
前面已經(jīng)講了執(zhí)行框架和執(zhí)行環(huán)境贩挣,現(xiàn)在就來了解一下運行時環(huán)境喉前。
Python
在初始化時會創(chuàng)建一個主線程,所以其運行時環(huán)境中存在一個主線程王财。Python
中的一個線程就是操作系統(tǒng)上的一個原生線程卵迂。
前面講了Python
虛擬機的運行框架(for
&switch/case
),這個運行框架就是對CPU
的抽象(就把這個執(zhí)行框架認為是軟CPU
就行)绒净。Python
中所有線程都使用這個軟CPU
來完成計算工作见咒。
真實機器上的任務切換機制對應到Python
中,就是使不同的線程輪流使用虛擬機的機制疯溺。
CPU
切換任務時需要保存線程上下文環(huán)境论颅,對于Python
來說,切換線程之前也需要保存當前線程信息囱嫩。Python
中使用PyThreadState
對象來保存線程狀態(tài)信息恃疯。一個線程擁有一個PyThreadState
對象。
Python
對于進程的抽象是由PyInterpreterState
對象來實現(xiàn)的墨闲。一個進程中可以有多個線程今妄,線程的同步通過全局解釋器鎖GIL
(Global Interpreter Lock
)來實現(xiàn)。
typedef struct _is {
struct _is *next;
struct _ts *tstate_head; // 模擬進程環(huán)境中的線程集合
PyObject *modules;
PyObject *sysdict;
PyObject *builtins;
PyObject *modules_reloading;
PyObject *codec_search_path;
PyObject *codec_search_cache;
PyObject *codec_error_registry;
} PyInterpreterState;
typedef struct _ts {
struct _ts *next;
PyInterpreterState *interp;
struct _frame *frame; // 模擬線程中的函數(shù)調(diào)用堆棧
int recursion_depth;
int tracing;
int use_tracing;
Py_tracefunc c_profilefunc;
Py_tracefunc c_tracefunc;
PyObject *c_profileobj;
PyObject *c_traceobj;
PyObject *curexc_type;
PyObject *curexc_value;
PyObject *curexc_traceback;
PyObject *exc_type;
PyObject *exc_value;
PyObject *exc_traceback;
PyObject *dict; /* Stores per-thread state */
int tick_counter;
int gilstate_counter;
PyObject *async_exc; /* Asynchronous exception to raise */
long thread_id; /* Thread id where this tstate was created */
int trash_delete_nesting;
PyObject *trash_delete_later;
} PyThreadState;
- 在每個
PyThreadState
對象中鸳碧,會維護一個棧幀列表盾鳞,以與PyThreadState
對象的線程中的函數(shù)調(diào)用機制對應。 - 在
PyEval_EvalFrameEx()
函數(shù)中瞻离,會將當前線程狀態(tài)對象(PyThreadState
)中的frame
設置為當前的執(zhí)行難環(huán)境(frame
)腾仅。
// ceval.c
PyObject *
PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
{
......
// 獲取當前活動線程的線程狀態(tài)對象(PyThreadState)
PyThreadState *tstate = PyThreadState_GET();
// 設置線程狀態(tài)對象中的frame
tstate->frame = f;
co = f->f_code;
names = co->co_names;
consts = co->co_consts;
why = WHY_NOT;
......
for (;;) {
fast_next_opcode:
f->f_lasti = INSTR_OFFSET();
// 獲取字節(jié)碼指令
opcode = NEXTOP();
oparg = 0;
// 如果指令有參數(shù),獲取參數(shù)
if (HAS_ARG(opcode))
oparg = NEXTARG();
dispatch_opcode:
......
}
}
- 而在建立新的
PyFrameObject
對象時套利,則從當前活動線程的線程狀態(tài)對象PyThreadState
中取出舊的frame
推励,建立PyFrameObject
鏈表鹤耍。
歡迎關注微信公眾號(coder0x00)或掃描下方二維碼關注,我們將持續(xù)搜尋程序員必備基礎技能包提供給大家验辞。