線程棧(thread stacks)介紹
先回顧下linux的內存空間布局
當啟動一個C實現的thread時,C標准庫會負責分配一塊內存作為這個線程的棧。標准庫分配這塊內存,告訴內核它的位置並讓內核處理這個線程 的執行。
在linux系統中,可通過 ulimit -s查看系統棧大小(8M)。ulimit -s 10240可修改棧大小為10M。
這里最大的一個問題是,分配大數組,或者循環遞歸函數時,默認的棧空間不夠用,會導致Segmentation fault錯誤。
//testMaxStack.cpp #include <stdio.h> int main() { printf("init ok\n"); char a[8192*1024]; // 8M空間 printf("run over\n"); } //執行結果 [app@VM_114_13_centos c]$ ulimit -s 8192 [app@VM_114_13_centos c]$ g++ testMaxStack.cpp [app@VM_114_13_centos c]$ ./a.out Segmentation fault
解決方法有兩個:
ulimit -s 10240調整標准庫給所有線程棧分配的內存塊的大小。但是全線提高棧大小意味着每個線程都會提高棧的內存使用量,這樣一來,你將用光所有內存。- 為每個線程單獨確定棧大小。這樣一來你就不得不完成這樣的任務:根據每個線程的需要,估算它們的棧內存的大小。這將是創建線程的難度超出我們的期望。
Go是如何應對這個問題的
Go使用的解決方案類似第二種方法。
goroutine 初始時只給棧分配很小的空間,然后隨着使用過程中的需要自動地增長。這就是為什么Go可以開千千萬萬個goroutine而不會耗盡內存。
Go 1.4開始使用的是連續棧,而這之前使用的分段棧。
分段棧(Segmented Stacks)
分段棧(segmented stacks)是Go語言最初用來處理棧的方案。
當創建一個goroutine時,Go運行時會分配一段8K字節的內存用於棧供goroutine運行使 用。
每個go函數在函數入口處都會有一小段代碼,這段代碼會檢查是否用光了已分配的棧空間,如果用光了,這段代碼會調用morestack函數。
morestack函數
morestack函數會分配一段新內存用作棧空間,接下來它會將有關棧的各種數據信息寫入棧底的一個struct中(下圖中Stack info),包括上一段棧的地址。然后重啟goroutine,從導致棧空間用光的那個函數(下圖中的Foobar)開始執行。這就是所謂的“棧分裂 (stack split)”。
+---------------+
| | | unused | | stack | | space | +---------------+ | Foobar | | | +---------------+ | | | lessstack | +---------------+ | Stack info | | |-----+ +---------------+ | | | +---------------+ | | Foobar | | | | <---+ +---------------+ | rest of stack | | |
lessstack函數
在新棧的底部,插入了一個棧入口函數lessstack。設置這個函數用於從那個導致我們用光棧空間的函數(Foobar)返回時用的。當那個函數(Foobar)返回時,我們回到lessstack(這個棧幀),lessstack會查找 stack底部的那個struct,並調整棧指針(stack pointer),使得我們返回到前一段棧空間。這樣做之后,我們就可以將這個新棧段(stack segment)釋放掉,並繼續執行我們的程序了。
分段棧的問題
棧縮小是一個相對代價高昂的操作。如果在一個循環中調用的函數遇到棧分裂 (stack split),進入函數時會增加棧空間(morestack 函數),返回並釋放棧段(lessstack 函數)。性能方面開銷很大。
連續棧(continuous stacks)
go現在使用的是這套解決方案。
goroutine在棧上運行着,當用光棧空間,它遇到與舊方案中相同的棧溢出檢查。但是與舊方案采用的保留一個返 回前一段棧的link不同,新方案創建一個兩倍於原stack大小的新stack,並將舊棧拷貝到其中。
這意味着當棧實際使用的空間縮小為原先的 大小時,go運行時不用做任何事情。
棧縮小是一個無任何代價的操作(棧的收縮是垃圾回收的過程中實現的.當檢測到棧只使用了不到1/4時,棧縮小為原來的1/2)。
此外,當棧再次增長時,運行時也無需做任何事情,我們只需要重用之前分配的空閑空間即可。
如何捕獲到函數的棧空間不足
Go語言和C不同,不是使用棧指針寄存器和棧基址寄存器確定函數的棧的。
在Go的運行時庫中,每個goroutine對應一個結構體G,大致相當於進程控制塊的概念。這個結構體中存了stackbase和stackguard,用於確定這個goroutine使用的棧空間信息。每個Go函數調用的前幾條指令,先比較棧指針寄存器跟g->stackguard,檢測是否發生棧溢出。如果棧指針寄存器值超越了stackguard就需要擴展棧空間。
舊棧數據復制到新棧
舊棧數據復制到新棧的過程,要考慮指針失效問題。
Go實現了精確的垃圾回收,運行時知道每一塊內存對應的對象的類型信息。在復制之后,會進行指針的調整。具體做法是,對當前棧幀之前的每一個棧幀,對其中的每一個指針,檢測指針指向的地址,如果指向地址是落在舊棧范圍內的,則將它加上一個偏移使它指向新棧的相應地址。這個偏移值等於新棧基地址減舊棧基地址。
鏈接:https://www.jianshu.com/p/7ec9acca6480
