深入理解golang 的棧
執行緒棧(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實現了精確的垃圾回收,執行時知道每一塊記憶體對應的物件的型別資訊。在複製之後,會進行指標的調整。具體做法是,對當前棧幀之前的每一個棧幀,對其中的每一個指標,檢測指標指向的地址,如果指向地址是落在舊棧範圍內的,則將它加上一個偏移使它指向新棧的相應地址。這個偏移值等於新棧基地址減舊棧基地址。
相關文章
- 深入理解golang:ContextGolangContext
- 深入理解 Golang 之 contextGolangContext
- 深入理解 Golang 指標Golang指標
- Golang 物件導向深入理解Golang物件
- 深入理解golang:sync.mapGolang
- 深入理解Golang之interface和reflectGolang
- [視訊版]-Golang深入理解GMPGolang
- 深入理解golang:記憶體分配原理Golang記憶體
- 棧、堆、佇列深入理解,面試無憂佇列面試
- golang 快速入門 [8.3]-深入理解浮點數Golang
- (轉)解密 Golang 的 Request 物件:深入理解 HTTP 請求的關鍵解密Golang物件HTTP
- 深入理解 JavaScript 執行上下文和執行棧JavaScript
- 深入理解JavaScript執行上下文和執行棧JavaScript
- 深入理解python虛擬機器:程式執行的載體——棧幀Python虛擬機
- 深入分析 Golang 的 ErrorGolangError
- 理解Golang的Time結構Golang
- Golang中閉包的理解Golang
- 【GoLang 那點事】深入淺出那些你知道但不理解的併發模型Golang模型
- golang學習筆記(二)—— 深入golang中的協程Golang筆記
- golang當中對select的理解Golang
- Golang 檔案操作的深入研究Golang
- JavaScript理解堆和棧JavaScript
- JS中this的深入理解JS
- 深入理解Js中的thisJS
- 徹底理解Golang MapGolang
- 理解Golang多重賦值Golang賦值
- [譯]深入理解JavaScript函式執行—呼叫棧,事件迴圈和任務等JavaScript函式事件
- 深入理解 React 的 useSyncExternalStore HookReactHook
- 深入理解Django的ModelForm操作DjangoORM
- 深入理解python中的yieldPython
- 深入理解Java中的鎖Java
- 深入理解Flutter的GestureDetector元件Flutter元件
- 深入理解Flutter的Listener元件Flutter元件
- 深入理解Java中的AQSJavaAQS
- 深入理解kestrel的應用
- JSON.stringify() 的深入理解JSON
- 深入理解swift的閉包Swift
- 深入理解 Java 中的 LambdaJava