標籤:occurred 參考 ror target 直接 tps pre pil 分享圖片
Python位元組碼
我們知道,Python原始碼在執行前,會先將原始碼編譯為位元組碼序列,Python虛擬機器就根據這些位元組碼進行一系列的操作,從而完成對Python程式的執行。在Python2.5中,一共定義了104條位元組碼指令:
opcode.h
#define STOP_CODE0#define POP_TOP1#define ROT_TWO2#define ROT_THREE3#define DUP_TOP4#define ROT_FOUR5#define NOP9#define UNARY_POSITIVE10#define UNARY_NEGATIVE11#define UNARY_NOT12#define UNARY_CONVERT13#define UNARY_INVERT15#define LIST_APPEND18#define BINARY_POWER19…………#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_ARGUMENT90/* 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 = 1s = "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;//設定線程狀態物件中的frameco = f->f_code;names = co->co_names;consts = co->co_consts;…………//虛擬機器主迴圈for (;;) {opcode = NEXTOP();oparg = 0; /* allows oparg to be stored in a register becauseit 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運行時環境
Python虛擬機器架構