Cpython源码分析03(*)_简要总结下Cpython是如何执行python test.py
當我們通過命令行傳入參數的方式調用 python 解釋器去運行一個模塊的時候,比如: $ python test.py 圖2.1中所示的過程將開始進行。(當然這只是其中一種運行 Python 程序的方式比如也可以在交互模式下單行運行,對于交互模式,這里暫時不做討論。) 基于Python3.7
Python 可執行程序是一個用 C 語言編寫的程序。當它被執行的時候,所發生的事情其實就和其他 C 語言程序(比如 Linux 內核或是一個簡單的 hello world 程序)差不多。請花一點時間來理解一下,Python 可執行程序只是一個用來運行程序的程序,可以結合 C 與 匯編或者 LLVM 的關系來理解。當我們使用解釋器運行一個模塊的時候,最開始會運行一個與平臺相關的標準初始化過程。
所有討論都假設在類 Unix 操作系統中,Windows 下會有些許不同。
C 運行時環境會包辦了所有的初始化操作,像是加載庫、檢查與設置環境變量等等。然后 python 可執行程序的 main 方法開始運行就像任何其他普通的 C 程序那樣。
python 的 main 函數位于 Programs/python.c文件中。 main 函數會調用位于 Modules/main.c中的 Py_Main 函數,它處理解釋器的初始化過程,包括:解析命令行參數、設置程序的 flags、讀取環境變量、運行 hook、哈希初始化等等。作為初始化過程的一部分, Python/pylifecycle.c中的 Py_Initialize 函數會被調用,它會初始化兩個比較重要的數據結構:解釋器狀態與線程狀態。
看一眼解釋器狀態與線程狀態的定義,它能給我們一些關于它們功能的信息。這兩個數據結構由一些指向特定字段的指針組成,它們帶有程序執行所需要的一些信息。首先我們來看解釋器狀態在源碼中的定義(Include/pystate.h),以下是其中比較重要的部分:
Python 程序員應該或多或少會對這些字段名字中的詞有一些認識比如:sysdict,builtins,codec 等。
- *next 字段為一個指向另一個解釋器實例的引用(譯注:多個解釋器可以同時存在于一個進程當中,這個特性有望用于解決 CPython 的 GIL 問題,參考 PEP554)
- *tstate_head 字段指向運行的主線程。當程序開啟多線程時,解釋器被它開啟的所有線程所共享。線程狀態隨后將被討論。
- modules, modules_by_index, sysdict, builtins 以及 importlib 從它們的名字就能理解。它們都被定義為 PyObject 的實例,在 Python 虛擬機中 PyObject 為最基本的對象類型。
- codec* 相關的字段持有用于定位、加載編碼方式(encodings)的信息,對于字節解碼(decoding bytes)非常重要。
程序的運行必須在一個線程之中進行,thread state structure 包含了一個線程執行 python code object 所需的信息,關于 thread state 定義的其中一部分代碼如下(Include/pystate.h):
//線程狀態數據結構定義如下 typedef struct _ts {struct _ts *prev;struct _ts *next;PyInterpreterState *interp;struct _frame *frame;int recursion_depth;char overflowed; /* The stack has overflowed. Allow 50 more callsto handle the runtime error. */char recursion_critical; /* The current calls must not causea stack overflow. */int stackcheck_counter;/* 'tracing' keeps track of the execution depth when tracing/profiling.This is to prevent the actual trace/profile code from being recorded inthe trace/profile. */int tracing;int use_tracing;Py_tracefunc c_profilefunc;Py_tracefunc c_tracefunc;PyObject *c_profileobj;PyObject *c_traceobj;/* The exception currently being raised */PyObject *curexc_type;PyObject *curexc_value;PyObject *curexc_traceback;PyObject *dict; /* Stores per-thread state */int gilstate_counter; } PyThreadState;關于這兩個數據結構,后面的章節中還會有進一步討論。初始化過程還會設置一些重要的機制,比如基本的 stdio等。
一旦完成了所有的初始化,Py_Main 函數會調用 pymain_run_file函數,隨后發生調用:
PyRun_AnyFileExFlags -> PyRun_SimpleFileExFlags -> PyRun_FileExFlags -> PyParser_ASTFromFileObject在 PyRun_SimpleFileExFlags 函數調用中,main namespace 將被創建,文件內容將在其中被運行。它也將檢查文件的 pyc 版本是否存在(pyc 文件是一個儲存了已經被執行過的py文件編譯后的代碼(字節碼)的文件)。當文件的 pyc 版本存在的時候,將會嘗試以二進制形式讀取并執行它。而當 pyc 文件不存在的時候,會順著 PyRun_FileExFlags 向下調用, PyParser_ASTFromFileObject函數會接著調用 PyParser_ParseFileObject函數,它會讀取被執行模塊的內容并建立 parse tree,然后將 parse tree 傳遞給 PyAST_FromNodeObject根據 parse tree 創建 AST。(如果你閱讀這些代碼,你會看到很多的 Py_INCREF 和 Py_DECREF。這些內存管理函數會在后面詳細討論。CPython 通過引用計數管理對象的生命周期。當一個新的對象引用產生的時候,計數通過Py_INCREF增加,當一個引用離開作用域的時候會通過Py_DECREF減少。)
AST 生成后會被傳給 run_mod函數,這個函數會接著調用 PyAST_CompileObject 根據 AST 生成 code object。并且在調用 PyAST_CompileObject 生成字節碼的時候會經過一個簡單的 peephole optimizer 進行字節碼優化。隨后 run_mod 會調用 PyEval_EvalCode 隨之產生函數調用:
PyEval_EvalCode -> PyEval_EvalCodeEx -> _PyEval_EvalCodeWithName -> PyEval_EvalFrameEx- 在 PyEval_EvalFrameEx 中的解釋器循環(interpreter loop)才會實際進行對 code object 的運行處理。它不只是將 code object 作為參數而是使用一個 frame object,它有一個字段用來保存到 code object 的引用。簡單來說,解釋器循環會不斷讀取儲存在一個指令數組中的下一個指令進行執行,增加或者刪除位于棧上的對象,直到沒有新的指令或者中途發生異常中斷了循環。
- 頭文件 Include/opcode.h 中包含了 python 虛擬機支持的 instruction/opcodes 清單。其實 opcodes 的概念很容易理解,在上面的例子中有4條 instruction:LOAD_FAST 將它的參數對應的值(這里對應x)加載到棧上。python 虛擬機是基于棧的 (stack based),也就是說 opcode 進行求值的對象以及求值得到的結果都會保存在棧上。BINARY_MULTIPLY opcode 會將前兩條指令的結果從棧中彈出(pop),執行二進制乘法運算然后將結果放(push)回棧頂。RETURN_VALUE 指令會從棧中彈出設置為返回值并中斷解釋器循環。
當所有的指令都被執行以后,Py_Main 將繼續執行一些清理(clean up)過程。就像 Py_Initialize 所做的那樣,Py_Finalize 會被調用完成一些清理任務,比如等待線程退出、調用 exit hooks、清理解釋器分配的仍然被使用的內存等等。
上面我們在一個較高的層面上對 python 解釋器如何執行程序進行了描述,但還有大量的細節沒有被討論,接下來的文章中我們將深入進去提供更多的細節信息。
參考視頻
參考課程
參考文章
總結
以上是生活随笔為你收集整理的Cpython源码分析03(*)_简要总结下Cpython是如何执行python test.py的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: nrf51822杂乱笔记
- 下一篇: Python:pyinstaller库实