棧空間:用戶棧和內核棧 程式的執行流程 進程其實都是在執行任務,而任務其實就是函數定義的(函數也稱為方法、子程式等,本質都一樣),所以進程的作用就是不斷的執行函數。程式啟動時,第一個要執行的函數是main()函數(有些語言隱藏了這個函數,但任何程式一定會有一個程式入口函數),然後在main()函數中 ...
棧空間:用戶棧和內核棧
程式的執行流程
進程其實都是在執行任務,而任務其實就是函數定義的(函數也稱為方法、子程式等,本質都一樣),所以進程的作用就是不斷的執行函數。程式啟動時,第一個要執行的函數是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了。