Python虛擬機器架構(一)

來源:互聯網
上載者:User

標籤:運行環境   def   dstat   作業系統   原理   reference   開頭   ring   地址   

Python虛擬機器中的執行環境

Python的虛擬機器實際上是在類比作業系統運行可執行檔的過程,首先,我們先來講一下普通的x86的機器上,可執行檔是以一種什麼方式啟動並執行。

圖1-1

圖1-1所展示的運行時棧的情形可以看作是如下的C代碼運行時情形:

#include <stdio.h>void f(int a, int b){    printf("a=%d, b=%d\n", a, b);}void g(){    f(1, 2);}main(int argc, char const *argv[]){    g();    return 0;}

  

esp:棧指標寄存器(extended stack pointer),其記憶體放著一個指標,該指標永遠指向系統棧最上面一個棧幀的棧頂.

ebp:基址指標寄存器(extended base pointer),其記憶體放著一個指標,該指標永遠指向系統棧最上面一個棧幀的底部

當程式的流程進入函數f時,圖1-1,其中“調用者的幀”是函數g的棧幀,而“當前幀”則是函數f的棧幀。對於一個函數而言,其所有局部變數的操作都在自己的棧幀中完成,而函數間的調用則通過建立新的棧幀完成

圖1-1所示的系統中,運行時棧是從地址空間的高地址向低地址延伸的。當在函數g中執行函數f的調用時,系統就會在地址空間中,於g的棧幀之後,建立f的棧幀。當然,在發生函數調用時,系統會儲存上一個棧幀的棧指標esp和幀指標ebp。當函數f執行完成之後,系統會把esp和ebp的值恢複為建立f的棧幀之前的值。這樣,程式的流程又回到函數g中,而程式的工作空間則又回到函數g的棧幀中。這就是可執行檔再x86機器上的大致運行原理。而Python正是在虛擬機器中通過不同的實現方式類比了這一原理,從而完成了Python位元組碼指令序列的執行。

我們之前在Python之code對象與pyc檔案(一)、Python之code對象與pyc檔案(二)、Python之code對象與pyc檔案(三)中剖析了,PyCodeObject對象包含了程式中的靜態資訊,然而有一點PyCodeObject對象沒有包含,那就是關於程式運行時的動態信心——執行環境

什麼是執行環境呢?考慮下面的一個例子:

env.py

i = "Python"def f():    i = 999    print(i)  # <1>f()print(i)  # <2>

  

在代碼<1>和<2>兩個地方,都執行了同樣的動作,列印變數i的值。顯然,它們所對應的位元組碼指令肯定是相同的,但這兩條語句的執行效果肯定不同。正是因為執行環境的影響,所以在<1>處會列印999,在<2>處會列印Python。像這種同樣的符號在程式啟動並執行不同時刻對應不同的值,甚至不同類型的情況,必須在運行時動態捕捉和維護。這些資訊是不可能在PyCodeObject對象中被靜態儲存的

這裡簡單介紹Python中的一個名詞——名字空間:

  • python中,每個函數都有一個自己的名字空間,通過locals()訪問,它記錄了函數的變數
  • python中,每個module有一個自己的名字空間,通過globals()訪問,它記錄了module的變數,包括 functions, classes 和其它imported modules,還有 module層級的變數和常量
  • 還有一個builtins名字空間,可以被任意模組訪問,這個builtins名字空間儲存著 class object、class Exception、def len等一些基礎類型和基礎函數
x = 3def f1(x=1):    y = 2    print("locals:", locals())f1()print("globals:", globals())

  

運行結果:

locals: {‘y‘: 2, ‘x‘: 1}globals: {‘__name__‘: ‘__main__‘,…………, ‘x‘: 3, ‘f1‘: <function f1 at 0x000000DA28CE3E18>}

  

名字空間是執行環境的一部分,除了名字空間,在執行環境中,還包含了一些其他資訊

結合x86平台運行可執行檔的機理,我們可以用這樣的機理來解釋env.py的執行過程。當Python開始執行env.py中第一條運算式時,Python已經建立起一個執行環境A,所有的位元組碼指令都會在這個執行環境中執行。Python可以從這個執行環境中擷取變數的值,也可以根據位元組碼的指令修改執行環境中某個變數的值,以影響後續程式的運行。這樣的過程會一直持續下去,直到發生了函數的調用行為

當Python在執行環境A中執行調用函數f的位元組碼指令時,會在當前的執行環境A之外重新建立一個新的執行環境B,在這個新的執行環境B中,有一個新的名字為"i"的對象。所以,新的執行環境B可以對應圖1-1這種所示的新的棧幀

所以在Python真正執行的時候,它的虛擬機器實際上面對的並不是一個PyCodeObject對象,而是另外一個對象——PyFrameObject,它就是我們所說的執行環境,也是Python對x86平台上棧幀的類比

Python源碼中的PyFrameObject

Python源碼中PyFrameObject的定義:

frameobject.h

typedef struct _frame {    PyObject_VAR_HEAD    struct _frame *f_back;/* 執行環境鏈上的前一個frame */    PyCodeObject *f_code;/* PyCodeObject對象 */    PyObject *f_builtins;/* builtin名字空間 */    PyObject *f_globals;/* global名字空間 */    PyObject *f_locals;/* local名字空間 */    PyObject **f_valuestack;/* 運行時的棧底位置 */    PyObject **f_stacktop;  /* 運行時的棧頂位置 */    …………    int f_lasti;/* 上一條位元組碼指令在f_code中的位移位置 */    /* 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;/* 當前位元組碼對應的原始碼行 */    int f_iblock;/* index in f_blockstack */…………//動態記憶體、維護(局部變數+cell對象集合+free對象集合+運行時棧)所需要的空間    PyObject *f_localsplus[1];} PyFrameObject;

  

從f_back我們可以看出一點,在Python實際執行的過程中,會產生很多PyFrameObject對象,而這些對象會被連結起來,形成一條執行環境鏈表。這正是對x86機器上棧幀間關係的類比。在x86上,棧幀間通過esp指標和ebp指標建立關係,使得新棧幀結束後能返回舊棧幀中,而Python正是靠f_back來完成這個動作的

在f_code中存放的是一個待執行的PyCodeObject對象,而接下來的f_builtins、f_globals、f_locals是3個獨立的名字空間,如我們所說,名字空間是執行環境的一部分。當執行env.py時,當要列印i這個變數,會去f_locals中尋找i這個PyStringObject變數,找到後將其對應的值取出,再列印出來

在PyFrameObject開頭,有一個PyObject_VAR_HEAD,這表明PyFrameObject是一個變長對象,即每次建立PyFrameObject對象的大小可能是不一樣的,這些變動的記憶體是用來做什麼呢?實際上,每一個PyFrameObject對象都維護著一個PyCodeObject對象。這表明每一個PyFrameObject對象和Python源碼中的一段code都是對應的,更準確的說,是和我們研究PyCodeObject時提到的Code Block對應的。而在編譯一段Code Block時,會計算出這段Code Block執行過程中所需要的棧空間大小。這個棧空間大小儲存在PyCodeObject的co_stacksize中。因為不同的Code Block在執行時所需的棧空間大小不同,所以決定PyFrameObject的開頭一定有一個PyObject_VAR_HEAD

PyFrameObject對象是對x86機器上單個棧幀活動的類比,既然在x86的單個棧幀中,包含了計算所需的記憶體空間,為什麼執行計算還需要記憶體空間呢?舉個例子:在計算c=a+b時,我們需要將a和b的值讀入記憶體,然後計算結果也要存放在記憶體中,這些記憶體就是執行計算所必須的記憶體,然後計算結果也要存放在記憶體中,這些記憶體就是執行計算所必須的記憶體。所以,作為對x86棧幀的類比,在PyFrameObject中,也提供了這些對記憶體空間的類比。這裡,我們稱為“運行時棧”。注意:這裡的“運行時棧”的概念和x86平台上的“運行時棧”有所不同,我們這裡所謂的“運行時棧”單指運算時所需的記憶體空間

  

圖1-2

圖1-2展示了Python虛擬機器在某個運行時刻的完整運行環境

 

PyFrameObject中的動態記憶體空間

在PyFrameObject對象所維護的運行時棧中,儲存的都是PyObject *,f_localsplus維護著一段變動長度的記憶體,但這段記憶體並不只是給棧使用,還有別的對象也會使用

PyFrameObject *PyFrame_New(PyThreadState *tstate, PyCodeObject *code, PyObject *globals,    PyObject *locals){PyFrameObject *back = tstate->frame;PyFrameObject *f;PyObject *builtins;Py_ssize_t i;if (code->co_zombieframe != NULL) {                f = code->co_zombieframe;                code->co_zombieframe = NULL;                _Py_NewReference((PyObject *)f);                assert(f->f_code == code);}else {Py_ssize_t extras, ncells, nfrees;ncells = PyTuple_GET_SIZE(code->co_cellvars);nfrees = PyTuple_GET_SIZE(code->co_freevars);//四分部構成PyFrameObject維護的動態記憶體區,其大小由extras決定extras = code->co_stacksize + code->co_nlocals + ncells + nfrees;//計算初始化時運行時棧的棧頂extras = code->co_nlocals + ncells + nfrees;f->f_valuestack = f->f_localsplus + extras;}f->f_stacktop = f->f_valuestack;return f;} 

  

從上面的代碼可以知道,建立PyFrameObject對象時,額外申請的那部分記憶體中有一部分是給PyCodeObject對象中儲存的那些局部變數:co_freevars、co_cellvars。而另一部分才是給運行時棧使用的。所以,PyFrameObject對象中棧的起始位置(也就是棧底)是又f_valuestack維護的,而f_stacktop維護額當前的棧頂

圖1-3

圖1-3是一個剛被建立的PyFrameObject對象的,,從中可以看到運行時棧和PyFrameObject對象中動態記憶體部分的關係

在Python中訪問PyFrameObject對象

在Python中,有一種frame object,它是對C一級的PyFrameObject的封裝,而且Python還提供了一個方法能方便地獲得當前處於活動狀態的frame object。這個方法就是sys module中的_getframe方法

import sysvalue = 3def g():    frame = sys._getframe()    print("current function is:", frame.f_code.co_name)    caller = frame.f_back    print("caller function is:", caller.f_code.co_name)    print("caller‘s local namespace:", caller.f_locals)    print("caller‘s global namespace:", caller.f_globals.keys())def f():    a = 1    b = 2    g()def show():    f()show()

  

運行結果:

current function is: gcaller function is: fcaller‘s local namespace: {‘b‘: 2, ‘a‘: 1}caller‘s global namespace: dict_keys([‘__name__‘, ‘__doc__‘, ‘__package__‘, ‘__loader__‘, ‘__spec__‘, ‘__annotations__‘, ‘__builtins__‘, ‘__file__‘, ‘__cached__‘, ‘sys‘, ‘value‘, ‘g‘, ‘f‘, ‘show‘])

  

從執行結果可以看到,在函數f中可以通過caller完全獲得其調用者g函數的資訊,甚是是g的各個名字空間

Python虛擬機器架構(一)

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.