1. 程式人生 > >python3 原始碼閱讀-虛擬機器執行原理

python3 原始碼閱讀-虛擬機器執行原理

> 閱讀原始碼版本python 3.8.3 > > 參考書籍<> > > 參考書籍<> [官網文件目錄介紹](https://devguide.python.org/setup/) 1. Doc目錄主要是官方文件的說明。 2. Include:目錄主要包括了Python的執行的標頭檔案。 3. Lib:目錄主要包括了用Python實現的標準庫。 4. Modules: 該目錄中包含了所有用C語言編寫的模組,比如random、cStringIO等。Modules中的模組是那些對速度要求非常嚴格的模組,而有一些對速度沒有太嚴格要求的模組,比如os,就是用Python編寫,並且放在Lib目錄下的 5. Objects:該目錄中包含了所有Python的內建物件,包括整數、list、dict等。同時,該目錄還包括了Python在執行時需要的所有的內部使用物件的實現。 6. Parser:該目錄中包含了Python直譯器中的Scanner和Parser部分,即對Python原始碼進行詞法分析和語法分析的部分。除了這些,Parser目錄下還包含了一些有用的工具,這些工具能夠根據Python語言的語法自動生成Python語言的詞法和語法分析器,將python檔案編譯生成語法樹等相關工作。 7. Programs目錄主要包括了python的入口函式。 8. Python:目錄主要包括了Python動態執行時執行的程式碼,裡面包括編譯、位元組碼直譯器等工作。 ## 1. Run Python檔案的啟動流程 Python啟動是由Programs下的python.c檔案中的main函式開始執行 ```c /* Minimal main program -- everything is loaded from the library */ #include "Python.h" #include "pycore_pylifecycle.h" #ifdef MS_WINDOWS int wmain(int argc, wchar_t **argv) { return Py_Main(argc, argv); } #else int main(int argc, char **argv) { return Py_BytesMain(argc, argv); } #endif ``` ```c++ int Py_Main(int argc, wchar_t **argv) { ... return pymian_main(&args); } static int pymain_main(_PyArgv *args) { PyStatus status = pymain_init(args); // 初始化 if (_PyStatus_IS_EXIT(status)) { pymain_free(); return status.exitcode; } if (_PyStatus_EXCEPTION(status)) { pymain_exit_error(status); } return Py_RunMain(); } ``` ### 1.1 初始化關鍵流程 - 初始化一些與配置項 如:開啟utf-8模式,設定Python記憶體分配器 - 初始化`pyinit_core`核心部分 - 建立生命週期 `pycore_init_runtime`, 同時生成HashRandom - 初始化執行緒和直譯器並建立GIL鎖 `pycore_create_interpreter` - 初始化所有基礎型別,list, int, tuple等 `pycore_init_types` - 初始化sys模組 `_PySys_Create` - 初始化內建函式或者物件,如map, None, True等 `pycore_init_builtins` - 其中包括內建的錯誤型別初始化 `_PyBuiltins_AddExceptions` > Python3.8 對Python直譯器的初始化做了重構[PEP 587-Python初始化配置](https://www.python.org/dev/peps/pep-0587/) ### 1.2 run 相關原始碼閱讀 ```c int Py_RunMain(void) { int exitcode = 0; pymain_run_python(&exitcode); //執行python指令碼 if (Py_FinalizeEx() < 0) { // 釋放資源 /* Value unlikely to be confused with a non-error exit status or other special meaning */ exitcode = 120; } pymain_free(); // 釋放資源 if (_Py_UnhandledKeyboardInterrupt) { exitcode = exit_sigint(); } return exitcode; } static void pymain_run_python(int *exitcode) { // 獲取一個持有GIL鎖的直譯器 PyInterpreterState *interp = _PyInterpreterState_GET_UNSAFE(); /* pymain_run_stdin() modify the config */ ... // 新增sys_path等操作 if (config->
run_command) { // 命令列模式 *exitcode = pymain_run_command(config->run_command, &cf); } else if (config->run_module) { // 模組名 *exitcode = pymain_run_module(config->run_module, 1); } else if (main_importer_path != NULL) { *exitcode = pymain_run_module(L"__main__", 0); } else if (config->
run_filename != NULL) { // 檔名 *exitcode = pymain_run_file(config, &cf); } else { *exitcode = pymain_run_stdin(config, &cf); } ... } /* Parse input from a file and execute it */ //Python/pythonrun.c int PyRun_AnyFileExFlags(FILE *fp, const char *filename, int closeit, PyCompilerFlags *flags) { if (filename == NULL) filename = "???"; if (Py_FdIsInteractive(fp, filename)) { int err = PyRun_InteractiveLoopFlags(fp, filename, flags); // 是否是互動模式 if (closeit) fclose(fp); return err; } else return PyRun_SimpleFileExFlags(fp, filename, closeit, flags); // 執行指令碼 } // 執行python .py檔案 int PyRun_SimpleFileExFlags(FILE *fp, const char *filename, int closeit, PyCompilerFlags *flags) { ... if (maybe_pyc_file(fp, filename, ext, closeit)) { FILE *pyc_fp; /* Try to run a pyc file. First, re-open in binary */ ... v = run_pyc_file(pyc_fp, filename, d, d, flags); } else { /* When running from stdin, leave __main__.__loader__ alone */ ... v = PyRun_FileExFlags(fp, filename, Py_file_input, d, d, closeit, flags); } ... } PyObject * PyRun_FileExFlags(FILE *fp, const char *filename_str, int start, PyObject *globals, PyObject *locals, int closeit, PyCompilerFlags *flags) { ... // // 解析傳入的指令碼,解析成AST mod = PyParser_ASTFromFileObject(fp, filename, NULL, start, 0, 0, flags, NULL, arena); ... // 將AST編譯成位元組碼然後啟動位元組碼直譯器執行編譯結果 ret = run_mod(mod, filename, globals, locals, flags, arena); ... } // 檢視run_mode static PyObject * run_mod(mod_ty mod, PyObject *filename, PyObject *globals, PyObject *locals, PyCompilerFlags *flags, PyArena *arena) { ... // 將AST編譯成位元組碼 co = PyAST_CompileObject(mod, filename, flags, -1, arena); ... // 解釋執行編譯的位元組碼 v = run_eval_code_obj(co, globals, locals); Py_DECREF(co); return v; } ``` ### 1.3 位元組碼檢視案例 新建test.py ```python def show(a): return a if __name__ == "__main__": print(show(10)) ``` 執行命令: `python3 -m dis test.py` ```powershell λ ppython3 -m dis test.py 3 0 LOAD_CONST 0 () 2 LOAD_CONST 1 ('show') 4 MAKE_FUNCTION 0 6 STORE_NAME 0 (show) 7 8 LOAD_NAME 1 (__name__) 10 LOAD_CONST 2 ('__main__') 12 COMPARE_OP 2 (==) 14 POP_JUMP_IF_FALSE 28 8 16 LOAD_NAME 2 (print) 18 LOAD_NAME 0 (show) 20 LOAD_CONST 3 (10) 22 CALL_FUNCTION 1 24 CALL_FUNCTION 1 26 POP_TOP >> 28 LOAD_CONST 4 (None) ``` 左邊3, 7, 8表示 test.py中的第一行和第二行,右邊表示python byte code `Include/opcode.h` 發現總共有 163 個 opcode, 所有的 python 原始檔(Lib庫中的檔案)都會被編譯器翻譯成由 opcode 組成的 pyx 檔案,並**快取**在執行目錄,下次啟動程式**如果原始碼沒有修改過,則直接載入這個pyx檔案,這個檔案的存在可以加快 python 的載入速度**。普通.py檔案如我們的test.py 是直接進行編譯解釋執行的,不會生成.pyc檔案,想生成test.pyc 需要使用python內建的py_compile模組來編譯該檔案,或者執行命令`python3 -m test.py` [python生成.pyc檔案](https://www.cnblogs.com/zhangqunshi/p/6657208.html) ### 1.4 python中的code物件 位元組碼在python虛擬機器中對應的是`PyCodeObject`物件, .pyc檔案是位元組碼在磁碟上的表現形式。python編譯的過程中,一個程式碼塊就對應一個code物件,那麼如何確定多少程式碼算是一個Code Block呢? 編譯過程中遇到一個新的名稱空間或者作用域時就生成一個code物件,即類或函式都是一個程式碼塊,一個code的型別結構就是`PyCodeObject`, 參考[Junnplus](https://github.com/Junnplus/blog/issues/16) ```c /* Bytecode object */ typedef struct { PyObject_HEAD int co_argcount; /* #arguments, except *args */ // 位置引數的個數, int co_posonlyargcount; /* #positional only arguments */ int co_kwonlyargcount; /* #keyword only arguments */ int co_nlocals; /* #local variables */ int co_stacksize; /* #entries needed for evaluation stack */ int co_flags; /* CO_..., see below */ int co_firstlineno; /* first source line number */ PyObject *co_code; /* instruction opcodes */ PyObject *co_consts; /* list (constants used) */ PyObject *co_names; /* list of strings (names used) */ PyObject *co_varnames; /* tuple of strings (local variable names) */ PyObject *co_freevars; /* tuple of strings (free variable names) */ PyObject *co_cellvars; /* tuple of strings (cell variable names) */ /* The rest aren't used in either hash or comparisons, except for co_name, used in both. This is done to preserve the name and line number for tracebacks and debuggers; otherwise, constant de-duplication would collapse identical functions/lambdas defined on different lines. */ Py_ssize_t *co_cell2arg; /* Maps cell vars which are arguments. */ PyObject *co_filename; /* unicode (where it was loaded from) */ PyObject *co_name; /* unicode (name, for reference) */ PyObject *co_lnotab; /* string (encoding addr<->lineno mapping) See Objects/lnotab_notes.txt for details. */ void *co_zombieframe; /* for optimization only (see frameobject.c) */ PyObject *co_weakreflist; /* to support weakrefs to code objects */ /* Scratch space for extra data relating to the code object. Type is a void* to keep the format private in codeobject.c to force people to go through the proper APIs. */ void *co_extra; /* Per opcodes just-in-time cache * * To reduce cache size, we use indirect mapping from opcode index to * cache object: * cache = co_opcache[co_opcache_map[next_instr - first_instr] - 1] */ // co_opcache_map is indexed by (next_instr - first_instr). // * 0 means there is no cache for this opcode. // * n > 0 means there is cache in co_opcache[n-1]. unsigned char *co_opcache_map; _PyOpcache *co_opcache; int co_opcache_flag; // used to determine when create a cache. unsigned char co_opcache_size; // length of co_opcache. } PyCodeObject; ``` | Field | Content | Type | | ------------------ | ------------------------------------------------------------ | --------------- | | co_argcount | Code Block 的引數個數 | PyIntObject | | co_posonlyargcount | Code Block 的位置引數個數 | PyIntObject | | co_kwonlyargcount | Code Block 的關鍵字引數個數 | PyIntObject | | co_nlocals | Code Block 中區域性變數的個數 | PyIntObject | | co_stacksize | Code Block 的棧大小 | PyIntObject | | co_flags | N/A | PyIntObject | | co_firstlineno | Code Block 對應的 .py 檔案中的起始行號 | PyIntObject | | co_code | Code Block 編譯所得的位元組碼 | PyBytesObject | | co_consts | Code Block 中的常量集合 | PyTupleObject | | co_names | Code Block 中的符號集合 | PyTupleObject | | co_varnames | Code Block 中的區域性變數名集合 | PyTupleObject | | co_freevars | Code Block 中的自由變數名集合 | PyTupleObject | | co_cellvars | Code Block 中巢狀函式所引用的區域性變數名集合 | PyTupleObject | | co_cell2arg | N/A | PyTupleObject | | co_filename | Code Block 對應的 .py 檔名 | PyUnicodeObject | | co_name | Code Block 的名字,通常是函式名/類名/模組名 | PyUnicodeObject | | co_lnotab | Code Block 的位元組碼指令於 .py 檔案中 source code 行號對應關係 | PyBytesObject | | co_opcache_map | python3.8新增欄位,儲存位元組碼索引與CodeBlock物件的對映關係 | PyDictObject | #### 1.4.1 LOAD_CONST ```c // Python\ceval.c PREDICTED(LOAD_CONST); -> line 943: #define PREDICTED(op) PRED_##op: FAST_DISPATCH(); -> line 876 #define FAST_DISPATCH() goto fast_next_opcode ``` > 額外收穫: c 語言中 ##和# 號 在marco 裡的作用可以參考 [這篇 ](https://blog.csdn.net/huan447882949/article/details/76100155/) > > 在巨集定義裡, ## 被稱為*連線符(concatenator)* , a##b 表示將ab連線起來 > > #a 表示把a轉換成字串,即加雙引號, 所以LONAD_CONST這個指領根據巨集定義展開如下: ```c case TARGET(LOAD_CONST): { PRED_LOAD_CONST: PyObject *value = GETITEM(consts, oparg); // 獲取一個PyObject* 指標物件 Py_INCREF(value); // 引用計數加1 PUSH(value); // 把剛剛建立的PyObject* push到當前的frame的stack上, 以便下一個指令從這個 stack 上面獲取 goto fast_next_opcode; ``` ### 1.5 main_loop ```c++ // Python\ceval.c main_loop: for (;;) { ... switch (opcode) { /* BEWARE! It is essential that any operation that fails must goto error and that all operation that succeed call [FAST_]DISPATCH() ! */ case TARGET(NOP): { FAST_DISPATCH(); } case TARGET(LOAD_FAST): { PyObject *value = GETLOCAL(oparg); if (value == NULL) { format_exc_check_arg(PyExc_UnboundLocalError, UNBOUNDLOCAL_ERROR_MSG, PyTuple_GetItem(co->co_varnames, oparg)); goto error; } Py_INCREF(value); PUSH(value); FAST_DISPATCH(); } case TARGET(LOAD_CONST): { PREDICTED(LOAD_CONST); PyObject *value = GETITEM(consts, oparg); Py_INCREF(value); PUSH(value); FAST_DISPATCH(); } ... } } ``` 在 python 虛擬機器中,直譯器主要在一個很大的迴圈中,不停地讀入 opcode, 並根據 opcode 執行對應的指令,當執行完所有指令虛擬機器退出,程式也就結束了 ### 1.6 總結 ![image-20200608163433117.png](https://i.loli.net/2020/06/08/LMXfyB8ipJbwTuE.png) **過程描述:** 1. python先把程式碼(.py檔案)編譯成位元組碼,交給位元組碼虛擬機器,然後虛擬機器會從編譯得到的PyCodeObject物件中一條一條執行位元組碼指令,並在當前的上下文環境中執行這條位元組碼指令,從而完成程式的執行。Python虛擬機器實際上是在模擬操作中執行檔案的過程。PyCodeObject物件中包含了位元組碼指令以及程式的所有靜態資訊,但沒有包含程式執行時的動態資訊——執行環境(PyFrameObject),後面會繼續記錄執行環境的閱讀。 2. 從整體上看:OS中執行程式離不開兩個概念:程序和執行緒。python中模擬了這兩個概念,模擬程序和執行緒的分別是**PyInterpreterState**和**PyTreadState**。即:每個PyThreadState都對應著一個幀棧,python虛擬機器在多個執行緒上切換(**靠GIL實現執行緒之間的同步**)。當python虛擬機器開始執行時,它會先進行一些初始化操作,最後進入**PyEval_EvalFramEx**函式,內部實現了一個`main_loop`它的作用是不斷讀取編譯好的位元組碼,並一條一條執行,類似CPU執行指令的過程。函式內部主要是一個switch結構,根據位元組碼的不同執行不同的程式碼 ## 2. Python中的Frame 如上所說,`PyCodeObject`物件只是包含了位元組碼指令集以及程式的相關靜態資訊,虛擬機器的執行還需要一個執行環境,即`PyFrameObject`,也就是對系統棧幀的模擬。 ### 2.1 堆和棧的認識 > 堆中存的是物件。棧中存的是基本資料型別和堆中物件的引用。一個物件的大小是不可估計的,或者說是可以動態變化的,但是在棧中,一個物件只對應了一個4btye的引用(堆疊分離的好處) 記憶體中的堆疊和資料結構堆疊不是一個概念,可以說記憶體中的堆疊是真實存在的物理區,資料結構中的堆疊是抽象的資料儲存結構。 記憶體空間在邏輯上分為三部分:程式碼區,靜態資料區和動態資料區,動態資料區有分為堆區和棧區 - 程式碼區:儲存的二進位制程式碼塊,高階排程(作業排程)、中級排程(記憶體排程)、低階排程(程序排程)控制程式碼區執行程式碼的切換 - 靜態資料區:儲存全域性變數,靜態變數,常量,系統自動分配和回收。 - 動態資料區: - 棧區(stack):儲存執行方法的形參,區域性變數,返回值,有編譯器自動分配和回收,操作類似資料結構中的棧 - 堆區(heap):new一個物件的引用或者地址儲存在棧區,該地址指向指向物件儲存在堆區中的真實資料。如c中的`malloc`函式,python中的`Pymalloc` ![image.png](https://i.loli.net/2020/06/08/caVmlCSxw2fjBgd.png) ### 2.2 PyFrameObject物件 ```c typedef struct _frame{ PyObject_VAR_HEAD //"執行時棧"的大小是不確定的, 所以用可變長的物件 struct _frame *f_back; //執行環境鏈上的前一個frame,很多個PyFrameObject連線起來形成執行環境連結串列 PyCodeObject *f_code; //PyCodeObject 物件,這個frame就是這個PyCodeObject物件的上下文環境 PyObject *f_builtins; //builtin名字空間 PyObject *f_globals; //global名字空間 PyObject *f_locals; //local名字空間 PyObject **f_valuestack; //"執行時棧"的棧底位置 PyObject **f_stacktop; //"執行時棧"的棧頂位置 //... int f_lasti; //上一條位元組碼指令在f_code中的偏移位置 int f_lineno; //當前位元組碼對應的原始碼行 //... //動態記憶體,維護(區域性變數+cell物件集合+free物件集合+執行時棧)所需要的空間 PyObject *f_localsplus[1]; } PyFrameObject; ``` 如果你想知道 **PyFrameObject** 中每個欄位的意義, 請參考 [Junnplus' blog](https://github.com/Junnplus/blog/issues/22) 或者直接閱讀原始碼,瞭解frame的執行過程可以參考[zpoint'blog](https://github.com/zpoint/CPython-Internals/blob/master/Interpreter/frame/frame_cn.md). > 名字空間實際上是維護著變數名和變數值之間關係的PyDictObject物件。 > f_builtins, f_globals, f_locals名字空間分別維護了builtin, global, local的name與對應值之間的對映關係。 **每一個 PyFrameObject物件都維護了一個 PyCodeObject物件,這表明每一個 PyFrameObject中的動態記憶體空間物件都和原始碼中的一段Code相對應。** #### 2.2.1 棧幀的獲取,工作中會用到 可以通過sys._getframe([depth]), 獲取指定深度的`PyFrameObject`物件 ```powershell >>> import sys >>> frame = sys._getframe() >>> frame ``` #### 2.2.2 python中變數名的解析規則 LEGB **Local -> Enclosed -> Global -> Built-In** - **Local** 表示區域性變數 - **Enclosed** 表示巢狀的變數 - **Global** 表示全域性變數 - **Built-In** 表示內建變數 如果這幾個順序都取不到,就會丟擲 ValueError 可以在這個網站[python執行視覺化網站](http://pythontutor.com/visualize.html),觀察程式碼執行流程,以及變數的轉換賦值情況。 ## 3. 額外收穫 > **意外收穫:** 之前知道pythonGIL , 遇到I/O阻塞時會釋放gil,現在從原始碼中看到了對應的流程 ```c if (_Py_atomic_load_relaxed(&ceval->gil_drop_request)) { /* Give another thread a chance */ if (_PyThreadState_Swap(&runtime->gilstate, NULL) != tstate) { Py_FatalError("ceval: tstate mix-up"); } drop_gil(ceval, tstate); /* Other threads may run now */ take_gil(ceval, tstate); /* Check if we should make a quick exit. */ exit_thread_if_finalizing(runtime, tstate); if (_PyThreadState_Swap(&runtime->gilstate, tstate) != NULL) { Py_FatalError("ceval: orphan tstate"); } } /* Check for asynchronous exceptions. */ ``` 參考: [python 原始碼分析 基本篇](https://blog.csdn.net/qq_31720329/article/details/86751412) [python虛擬機器執行原理](https://www.cnblogs.com/webber1992/p/659716