1. 程式人生 > >每天3分鐘作業系統修煉祕籍(9):棧空間之使用者棧和核心棧

每天3分鐘作業系統修煉祕籍(9):棧空間之使用者棧和核心棧

棧空間:使用者棧和核心棧

程式的執行流程

程序其實都是在執行任務,而任務其實就是函式定義的(函式也稱為方法、子程式等,本質都一樣),所以程序的作用就是不斷的執行函式。程式啟動時,第一個要執行的函式是main()函式(有些語言隱藏了這個函式,但任何程式一定會有一個程式入口函式),然後在main()函式中呼叫其它函式,每當呼叫其它函式時,都會先進行函式跳轉,轉而讓程序去執行被呼叫的函式,當被調函式執行完成後又回到呼叫函式的位置繼續向下執行。

程式執行的基本流程如下圖所示。右邊是程式的虛擬碼,左邊是程式執行過程。首先程序跳轉到main函式處開始執行,然後執行一個賦值語句a=1,繼續往下發現是呼叫一個函式func1(),於是跳轉到func1(),同時還會儲存好main中是從這個位置(假設稱為位置1)處跳轉的,以便執行完func1()後可以跳回到main()。然後開始執行func1()中的程式碼,在CPU執行func1()執行的時候,main()函式就無法繼續向下執行了,它必須等待func1()執行完成後的返回,當func1()執行完後根據跳回到位置1,於是main函式繼續向下執行,也就是賦值語句x=2,然後又以同樣的流程呼叫func2()函式並返回,最終main()函式執行完成,程序終止,程式退出。

使用者棧和核心棧

使用者棧

每當程序呼叫一次函式,都會在使用者棧中為該函式分配一個棧幀(stack frame),也稱為呼叫棧(call stack),當該函式返回時又會釋放該棧幀。釋放的棧幀不會從虛擬記憶體中移除,它可以被之後呼叫的函式重新使用,所以棧空間的大小是不會減小的。

根據這個特性並結合上圖所描述的程式執行過程,可以推斷出一個重要的結論。由於函式內部呼叫函式時,外部函式的棧幀不會釋放,只有內部函式全部退出了才會繼續執行外部函式並在執行完成的時候釋放外部函式的棧幀,所以,遞迴函式(即函式內部呼叫函式自身)如果遞迴呼叫的層次太多(比如無限遞迴),會分配大量的棧幀,並且不會釋放,直到棧空間不足,無法再分配新的棧幀,這時會報棧溢位(stack overflows)錯誤。所以,必須要合理編寫遞迴函式,使得遞迴函式能夠在達到某些條件時返回,從而釋放棧幀,避免無限遞迴。

棧幀中儲存了傳遞給該函式的引數、該函式中定義的區域性變數、函式的返回值、呼叫該函式的程式計數器副本,以及一些其它重要資訊。這裡有必要解釋下棧幀中的程式計數器副本。

什麼是程式計數器(Program Counter,PC)?這是CPU中的一個暫存器,在這個暫存器中儲存了下一個要執行指令的指標。所以,CPU每執行一個指令的時候,就會設定這個暫存器使它指向下一個指令。

前面描述程式執行流程的時候說過,當main()函式呼叫func1()函式的時候,需要儲存main()函式中呼叫func1()的位置,以便func1()返回時可以跳轉回main()函式繼續向下執行。其實,main()函式在開始呼叫func1()函式的時候,PC暫存器就已經指向了這個指令,CPU可以將這個指令的指標的值(也就是PC的副本)儲存在func1()函式的棧幀中,這樣func1()執行完成後就能將這個指標重新放回到CPU的PC暫存器中,使得CPU重新回到main()函式呼叫func1()的位置處,從而呼叫者main可以取得函式func1()棧幀中的返回值(這時候func1()的棧幀被釋放),並繼續執行下面的程式碼。

核心棧

作業系統還為每個程序維護另一個棧:核心棧。這個棧的位置在核心的記憶體區域中,只有核心能夠訪問,使用者程序無法訪問。

核心棧的作用是存放上下文切換時的程序資訊。

當程序A要切換到程序B時,首先要陷入核心,然後核心將CPU中關於程序A的程序資訊(即某些暫存器中的值)儲存在程序A的核心棧中,然後從程序B的核心棧中恢復程序B的資訊到CPU的某些暫存器中,再退出核心模式回到程序B,這樣CPU就開始執行程序B了