Python字節碼
我們知道,Python源代碼在執行前,會先將源代碼編譯為字節碼序列,Python虛擬機就根據這些字節碼進行一系列的操作,從而完成對Python程序的執行。在Python2.5中,一共定義了104條字節碼指令:
opcode.h
#define STOP_CODE 0 #define POP_TOP 1 #define ROT_TWO 2 #define ROT_THREE 3 #define DUP_TOP 4 #define ROT_FOUR 5 #define NOP 9 #define UNARY_POSITIVE 10 #define UNARY_NEGATIVE 11 #define UNARY_NOT 12 #define UNARY_CONVERT 13 #define UNARY_INVERT 15 #define LIST_APPEND 18 #define BINARY_POWER 19 ………… #define CALL_FUNCTION_KW 141 /* #args + (#kwargs<<8) */ #define CALL_FUNCTION_VAR_KW 142 /* #args + (#kwargs<<8) */ /* Support for opargs more than 16 bits long */ #define EXTENDED_ARG 143
如果我們仔細看上面的字節碼指令,會發現雖然字節碼是從0定義到143,但中間有發生跳躍,比方5直接跳躍到9,13直接跳躍到15,15直接跳躍到18。所以,Python2.5實際上只定義了104條字節碼指令
在Python2.5的104條指令中,有一部分需要參數,另一部分是沒有參數的。所有需要參數的字節碼指令的編碼都是大於90。Python中提供了專門的宏來判斷一條字節碼指令是否需要參數:
opcode.h
#define HAVE_ARGUMENT 90 /* Opcodes from here have an argument: */ #define HAS_ARG(op) ((op) >= HAVE_ARGUMENT)
我們在Python之code對象與pyc文件(一)、Python之code對象與pyc文件(二)和Python之code對象與pyc文件(三)介紹了PyCodeObject對象,這個對象是Python對源代碼進行編譯后在內存中產生的靜態對象,這個對象當然也包含了源代碼編譯后的字節碼,我們可以用Python提供的code對象解析工具dis對其進行解析
# cat demo.py i = 1 s = "Python" d = {} l = [] # python ………… >>> source = open("demo.py").read() >>> co = compile(source, "demo.py", "exec") >>> import dis >>> dis.dis(co) 1 0 LOAD_CONST 0 (1) 3 STORE_NAME 0 (i) 2 6 LOAD_CONST 1 ('Python') 9 STORE_NAME 1 (s) 3 12 BUILD_MAP 0 15 STORE_NAME 2 (d) 4 18 BUILD_LIST 0 21 STORE_NAME 3 (l) 24 LOAD_CONST 2 (None) 27 RETURN_VALUE
最左邊的一列是字節碼指令在源代碼中所對應的行數,左起第二列是當前字節碼在co_code中的偏移位置,第三列顯示了當前字節碼的指令,第四列是指令的參數,最后一列是計算后的實際參數
Python虛擬機的運行框架
當Python啟動后,首先會進行Python運行時環境的初始化。注意,這里的運行時環境與之前的章節《Python之code對象與pyc文件》中的執行環境是不同的。運行時環境是一個全局的概念,而執行環境實際就是一個棧幀。是一個與某個Code Block對應的概念。而Python虛擬機的實現,是在一個函數中,這里我們列一下源碼,與實際的源代碼會做一些刪改:
ceval.c
PyObject * PyEval_EvalFrameEx(PyFrameObject *f, int throwflag) { ………… co = f->f_code; names = co->co_names; consts = co->co_consts; fastlocals = f->f_localsplus; freevars = f->f_localsplus + co->co_nlocals; first_instr = (unsigned char*) PyString_AS_STRING(co->co_code); next_instr = first_instr + f->f_lasti + 1; stack_pointer = f->f_stacktop; assert(stack_pointer != NULL); f->f_stacktop = NULL; ………… }
PyEval_EvalFrameEx首先會初始化一些變量,其中PyFrameObject對象中的PyCodeObject對象包含的重要信息都被照顧到了。當然,另一個重要的動作就是初始化了堆棧的棧頂指針stack_pointer,使其指向f->f_stacktop。PyCodeObject對象中的co_code域中保存着字節碼指令和字節碼指令的參數,Python虛擬機執行字節碼指令序列的過程就是從頭到尾遍歷整個co_code、依次執行字節碼指令的過程
在Python虛擬機中,利用3個變量來完成整個遍歷過程。co_code實際上是一個PyStringObject對象,而其中的字符數組才是真正有意義的東西,整個字節碼指令序列實際上在C中就是一個字符數組。因此,遍歷過程中所使用的3個變量都是char *類型的變量,first_instr永遠指向字節碼指令序列的開始位置,next_instr永遠指向下一條待執行的字節碼指令的位置,f_lasti指向上一條已經執行過的字節碼指令的位置
圖1-1 遍歷字節碼指令序列
圖1-1展示了3個變量在遍歷中某時刻的情景
Python虛擬機執行字節碼指令的架構,其實就是一個for循環加上一個巨大的switch/case結構:
ceval.c
PyObject *PyEval_EvalFrameEx(PyFrameObject *f, int throwflag) { ………… why = WHY_NOT; for (;;) { ………… fast_next_opcode: f->f_lasti = INSTR_OFFSET(); //獲得字節碼指令 opcode = NEXTOP(); oparg = 0; //如果指令需要參數,獲取指令參數 if (HAS_ARG(opcode)) oparg = NEXTARG(); dispatch_opcode: switch (opcode) { case NOP: goto fast_next_opcode; case LOAD_FAST: ………… } ………… } ………… }
上面的代碼只是一個極度簡化之后的Python虛擬機的樣子,完整的代碼實現在ceval.c文件的PyEval_EvalFrameEx方法中
在這個執行架構中,對字節碼的一步一步地遍歷是通過幾個宏來實現的:
ceval.c
#define INSTR_OFFSET() ((int)(next_instr - first_instr)) #define NEXTOP() (*next_instr++) #define NEXTARG() (next_instr += 2, (next_instr[-1]<<8) + next_instr[-2])
在對PyCodeObject對象分析中我們說過,Python字節碼有的是帶參數的,有的是沒帶參數的,判斷字節碼是否帶參數具體參考HAS_ARG這個宏的實現,對於不同字節碼指令,由於存在是否需要指令參數的區別,所以next_instr的位移可能是不同的,但無論如何,next_instr總是指向Python下一條要執行的字節碼
Python在獲得了一條字節碼和其需要的指令參數后,會對字節碼指令利用switch進行判斷,根據判斷的結果選擇不同的case語句,每一條字節碼指令都會對應一個case語句。在case語句中,就是Python對字節碼指令的實現
在成功執行完一條字節碼指令后,Python的執行流程會跳轉到fast_next_opcode處,或者是for循環處,不管如何,Python接下來的動作都是獲得下一條字節碼指令和指令參數,完成對下一條指令的執行。如此一條一條地遍歷co_code中包含的所有字節碼指令,最終完成了對Python程序的執行
這里還需要提到一個變量"why",它指示了退出這個巨大的for循環時Python執行引擎的狀態,因為Python執行引擎不一定每次執行都會正確無誤,很有可能在執行某條字節碼時產生了錯誤,這就是我們熟悉的異常——exception。所以在Python退出執行引擎的時候,就需要知道執行引擎是因為什么而結束的,是正常結束呢?還是因為錯誤的發生,無法執行下去了?why就承擔起這一重則
變量why的取值范圍在ceval.c中被定義,其實也是Python結束字節碼執行時的狀態:
ceval.c
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 */ };
Python運行時環境初探
前面我們說過,PyFrameObject對應於可執行文件在執行時的棧幀,但一個可執行文件要在操作系統中運行只有棧幀是不夠的,我們還忽略了兩個對於可執行文件至關重要的概念:進程和線程。Python在初始化時會創建一個主線程,所以其運行環境中存在一個主線程。因為在后面剖析Python異常機制會利用到Python內部的線程模型,因此,我們需要對Python線程模型有一個整體概念上的了解。
以Win32平台為例,我們知道,對於原生Win32可執行文件,都會在一個進程內執行。進程並非是與機器指令序列相對應的活動對象,這個可執行文件中機器指令序列對應的活動對象是由線程這個概念來進行抽象的,而進程則是線程的活動環境
對於通常的單線程可執行文件,在執行時操作系統會創建一個進程,在進程中,又會有一個主線程,而對於多線程的可執行文件,在執行時操作系統會創建出一個進程和多個線程,該多個線程能共享進程地址空間中的全局變量,這就自然而然地引出線程同步的問題。CPU對任務的切換實際上是在線程間切換,在切換任務時,CPU需要執行線程環境的保存工作,而在切換至新線程后,需要恢復該線程的線程環境
前面我們所看到的Python虛擬機的運行框架,實際上就是對CPU的抽象,可以看做一個軟CPU,Python中所有線程都使用這個軟CPU來完成計算工作。真實機器的任務切換機制對應到Python中,就是使不同的線程輪流使用虛擬機的機制
CPU切換任務時需要保存線程運行環境。對於Python來說,在切換線程之前,同樣需要保存關於當前線程的信息。在Python中,這個關於線程狀態信息的抽象是通過PyThreadState對象來實現的,一個線程將擁有一個PyThreadState對象。所以從另一種意義來說,這個PyThreadState對象也可以看成是線程本身的抽象。但實際上,這兩者是有很大的區別的,PyThreadState並非是對線程本身的模擬,因為Python中的線程仍然使用操作系統的原生線程,PyThreadState僅僅是對線程狀態的抽象
在Win32下,線程是不能獨立存活的,它需要存活在進程的環境中,而多個線程可以共享進程的一些資源。在Python中也是一樣,如果Python程序中有兩個線程,都會進行同樣一個動作——import sys,那么這個sys module應該存多少份?是全局共享還是每個線程但單獨一個sys module?如果每個線程單獨一份sys module,那么對Python內存的消耗會非常的驚人,所以在Python中,module都是全局共享的,仿佛這些module都是進程中的共享資源一樣,對於進程這個概念,Python以PyInterpreterState對象來實現
在Win32下,通常都會有多個進程,而Python實際上也可以由多個邏輯上的interpreter存在。在通常情況下,Python只有一個interpreter,這個interpreter中維護了一個或多個的PyThreadState對象,與這些PyThreadState對象對應的線程輪流使用一個字節碼執行引擎
現在,展示一下剛提到的表示進程概念的PyInterpreterState對象和表示線程概念的PyThreadState對象:
pystate.h
typedef struct _is { struct _is *next; struct _ts *tstate_head; //模擬進程環境中的線程集合 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 { /* See Python/ceval.c for comments explaining most fields */ struct _ts *next; PyInterpreterState *interp; struct _frame *frame; //模擬線程中的函數調用堆棧 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 */ } PyThreadState;
在PyThreadState對象中,我們看到熟悉的PyFrameObject(_frame)對象。也就是說,在每個PyThreadState對象中,會維護一個棧幀列表,以與PyThreadState對象的線程中的函數調用機制對應。在Win32上,情形也是一樣,每個線程都會有一個函數調用棧
當Python虛擬機開始執行時,會將當前線程狀態對象中的frame設置為當前的執行環境(frame):
PyObject *PyEval_EvalFrameEx(PyFrameObject *f, int throwflag) { ………… //通過PyThreadState_GET獲得當前活動線程對應的線程狀態對象 PyThreadState *tstate = PyThreadState_GET(); tstate->frame = f; //設置線程狀態對象中的frame co = f->f_code; names = co->co_names; consts = co->co_consts; ………… //虛擬機主循環 for (;;) { opcode = NEXTOP(); oparg = 0; /* allows oparg to be stored in a register because it doesn't have to be remembered across a full loop */ if (HAS_ARG(opcode)) oparg = NEXTARG(); //指令分派 switch (opcode) { ………… } ………… } ………… }
而在建立新的PyFrameObject對象時,則從當前線程的狀態對象中取出舊的frame,建立PyFrameObject鏈表:
PyFrameObject * PyFrame_New(PyThreadState *tstate, PyCodeObject *code, PyObject *globals, PyObject *locals) { //從PyThreadState中獲得當前線程的當前執行環境 PyFrameObject *back = tstate->frame; PyFrameObject *f; ………… //創建新的執行環境 f = PyObject_GC_Resize(PyFrameObject, f, extras); ………… //鏈接當前執行環境 f->f_back = back; f->f_tstate = tstate; return f; }
圖1-2Python運行時環境