計算機里面的棧其實有着舉足輕重的作用。大學剛學c語言的時候,教的是堆棧,傳達的是一種后入先出的算法思想。但其實我們知道,堆和棧是兩個截然不同的東西。而這里面說到的棧,則是更融入到計算機系統里面,CPU結構的一部分。
一個函數設計里面,有2個問題:
1.是參數傳遞的問題。傳遞參數的目的,是為了代碼可以重用,讓一種方法可以應用到更多的場合,而不需要為N種情況寫N套類似的代碼。那用什么方法來做參數的傳遞,可以選擇:
a.為了速度快,使用cpu的寄存器傳遞參數。這會碰到一個問題,cpu寄存器的數量是有限的,當函數內再想調用子函數的時候,再使用原有的cpu寄存器就會沖突了。想利用寄存器傳參,就必須在調用子函數前吧寄存器存儲起來,然后當函數退出的時候再恢復。
b.利用某些ram的區域來傳遞參數。這和上面a的情況幾乎一樣,當函數嵌套調用的時候,還是會出現沖突,依然面臨要把原本數據保存到其他地方,再調用嵌套函數。並且保存到什么地方,也面臨困難,無論臨時存儲到哪里,都會有上面傳遞參數一樣的困境。
2.函數里面必然要使用到局部變量,而不能總是用全局變量。則局部變量存儲到哪里合適,即不能讓函數嵌套的時候有沖突,又要注重效率。
以上問題的解決辦法,都可以利用棧的結構體來解決,寄存器傳參的沖突,可以把寄存器的值臨時壓入棧里面,非寄存器傳參也可以壓入到棧里面,局部變量的使用也可以利用棧里面的內存空間,只需要移動下棧指針,騰出局部變量占用的空間。最后利用棧指針的偏移來完成存取。於是函數的這些參數和變量的存儲演變成記住一個棧指針的地址,每次函數被調用的時候,都配套一個棧指針地址,即使循環嵌套調用函數,只要對應函數棧指針是不同的,也不會出現沖突。利用棧,當函數不斷調用的時候,不斷的有參數局部變量入棧,棧里面會形成一個函數棧幀的結構,一個棧幀結構歸屬於一次函數的調用。棧的空間也是有限的,如果不限制的使用,就會出現典型的棧溢出的問題。有了棧幀的框架在,我們在分析問題的時候,如果能獲取到當時的棧的內容,則有機會調查當時可能出現的問題。
經過上面的簡單介紹,應該可以看出棧在程序設計里面的作用,它是每個函數架構的基礎,有了它,才可以實現函數的重復利用。而為了更高的提高效率,每個cpu在設計的時候都有自己獨立的堆棧指令,例如push pop,有堆棧寄存器存儲堆棧指針,如ARM的R13寄存器,來盡可能的加速對棧的操作。
但這是在匯編機器語言的角度上看到的情況,在c語言的角度上看,明顯有意隱藏了棧的存在,這也是高級語言的意義,讓我們更關注功能本身,而不是如何被翻譯成機器代碼。但是了解它也有重要的意義,像上面說道的,問題發生的時候,利用棧來了解問題發生的情況十分必要。
然而棧存在的意義還不止這點,有了它的存在,才能構建出操作系統的多任務模式。
讓我們看一下下面的一個main函數的調用實例,上面說的棧幀的情況依然,main函數調用A函數,調用B函數,再調用C函數,然后依次返回,試想當單cpu在main函數的框架運行的話,永遠都在main函數厄結構里面(假設main函數是個無限循環結構),始終是在一個任務范圍內,談不上多任務。即使有另外一個任務在等待狀態,如何在main函數里面跳轉到另一個任務。顯然在c語言的框架下,這無法實現,因為如果是函數調用關系,則本質上還是屬於main函數的任務里面,不能算多任務切換。但需要注意到一個事實,此刻的main函數任務本身其實和它的棧綁定在一起了,無論是如何調用子函數,無論如何入棧退棧,棧指針都在本棧的范圍內移動, 屬於本任務的局部變量也和任務本身綁定了。
main()
---->A()
----->B()
------C()
<-----
<-----B()
<----A()
main()
由此可以看出,一個任務狀態可以利用如下信息來表征:1.main函數體代碼。2.main的棧指針位置(即存儲了局部變量等信息)。3.當前cpu寄存器的信息。假如我們可以保存在這些信息,則完全可以強制讓cpu去做別的事情,只是將來想繼續執行main任務的時候,把上面的信息恢復就可以。有了這樣的先決條件,多任務就有了存在的基礎。也可以看出棧存在的意義所在。在多任務模式下,當CPU認為有必要切換到別的任務上運行時,只需要保存好當前任務的狀態,即上面說的三個內容。恢復另一個任務的狀態,然后跳轉到上次運行的位置,就可以恢復繼續運行。可見每個任務都有自己的獨立的棧空間。正是有了獨立的棧空間,為了代碼重用,不同的任務甚至可以混用任務的函數體本身,例如可以一個main函數有2個任務實例。有不同的棧空間,這完全可以實現。
至此之后的操作系統的框架也形成了,譬如任務在調用sleep()等待的時候,可以主動讓出CPU給別的任務使用,或者分時操作系統任務在時間片用完是也會被迫的讓出cpu。不論是哪種方法,只要想辦法切換任務的上下文空間,切換棧即可。切換棧的實現並不能在c語言下完成,之前也說過c語言有意隱藏了棧的使用。所以在關鍵的上下文切換的地方,操作系統都需要用匯編代碼來實現。