深入理解golang 的棧

weixin_33716557發表於2018-11-06

執行緒棧(thread stacks)介紹

先回顧下linux的記憶體空間佈局


2905068-fc56b0549a856bb7.png
簡書_stack02.png

當啟動一個C實現的thread時,C標準庫會負責分配一塊記憶體作為這個執行緒的棧。標準庫分配這塊記憶體,告訴核心它的位置並讓核心處理這個執行緒 的執行。
在linux系統中,可通過 ulimit -s檢視系統棧大小(8M)。
ulimit -s 10240可修改棧大小為10M。

2905068-c681a0acfb3bda93.png
簡書_stack01.png

這裡最大的一個問題是,分配大陣列,或者迴圈遞迴函式時,預設的棧空間不夠用,會導致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,大致相當於程式控制塊的概念。這個結構體中存了stackbasestackguard,用於確定這個goroutine使用的棧空間資訊。每個Go函式呼叫的前幾條指令,先比較棧指標暫存器跟g->stackguard,檢測是否發生棧溢位。如果棧指標暫存器值超越了stackguard就需要擴充套件棧空間。

舊棧資料複製到新棧

舊棧資料複製到新棧的過程,要考慮指標失效問題。
Go實現了精確的垃圾回收,執行時知道每一塊記憶體對應的物件的型別資訊。在複製之後,會進行指標的調整。具體做法是,對當前棧幀之前的每一個棧幀,對其中的每一個指標,檢測指標指向的地址,如果指向地址是落在舊棧範圍內的,則將它加上一個偏移使它指向新棧的相應地址。這個偏移值等於新棧基地址減舊棧基地址

相關文章