深入理解golang 的栈
線(xiàn)程棧(thread stacks)介紹
先回顧下linux的內(nèi)存空間布局
簡(jiǎn)書(shū)_stack02.png
當(dāng)啟動(dòng)一個(gè)C實(shí)現(xiàn)的thread時(shí),C標(biāo)準(zhǔn)庫(kù)會(huì)負(fù)責(zé)分配一塊內(nèi)存作為這個(gè)線(xiàn)程的棧。標(biāo)準(zhǔn)庫(kù)分配這塊內(nèi)存,告訴內(nèi)核它的位置并讓內(nèi)核處理這個(gè)線(xiàn)程 的執(zhí)行。
在linux系統(tǒng)中,可通過(guò)?ulimit -s查看系統(tǒng)棧大小(8M)。
ulimit -s 10240可修改棧大小為10M。
?
這里最大的一個(gè)問(wèn)題是,分配大數(shù)組,或者循環(huán)遞歸函數(shù)時(shí),默認(rèn)的棧空間不夠用,會(huì)導(dǎo)致Segmentation fault錯(cuò)誤。
//testMaxStack.cpp解決方法有兩個(gè):
- ulimit -s 10240調(diào)整標(biāo)準(zhǔn)庫(kù)給所有線(xiàn)程棧分配的內(nèi)存塊的大小。但是全線(xiàn)提高棧大小意味著每個(gè)線(xiàn)程都會(huì)提高棧的內(nèi)存使用量,這樣一來(lái),你將用光所有內(nèi)存。
- 為每個(gè)線(xiàn)程單獨(dú)確定棧大小。這樣一來(lái)你就不得不完成這樣的任務(wù):根據(jù)每個(gè)線(xiàn)程的需要,估算它們的棧內(nèi)存的大小。這將是創(chuàng)建線(xiàn)程的難度超出我們的期望。
Go是如何應(yīng)對(duì)這個(gè)問(wèn)題的
Go使用的解決方案類(lèi)似第二種方法。
goroutine 初始時(shí)只給棧分配很小的空間,然后隨著使用過(guò)程中的需要自動(dòng)地增長(zhǎng)。這就是為什么Go可以開(kāi)千千萬(wàn)萬(wàn)個(gè)goroutine而不會(huì)耗盡內(nèi)存。
Go 1.4開(kāi)始使用的是連續(xù)棧,而這之前使用的分段棧。
分段棧(Segmented Stacks)
分段棧(segmented stacks)是Go語(yǔ)言最初用來(lái)處理?xiàng)5姆桨浮?br />當(dāng)創(chuàng)建一個(gè)goroutine時(shí),Go運(yùn)行時(shí)會(huì)分配一段8K字節(jié)的內(nèi)存用于棧供goroutine運(yùn)行使 用。
每個(gè)go函數(shù)在函數(shù)入口處都會(huì)有一小段代碼,這段代碼會(huì)檢查是否用光了已分配的棧空間,如果用光了,這段代碼會(huì)調(diào)用morestack函數(shù)。
morestack函數(shù)
morestack函數(shù)會(huì)分配一段新內(nèi)存用作棧空間,接下來(lái)它會(huì)將有關(guān)棧的各種數(shù)據(jù)信息寫(xiě)入棧底的一個(gè)struct中(下圖中Stack info),包括上一段棧的地址。然后重啟goroutine,從導(dǎo)致棧空間用光的那個(gè)函數(shù)(下圖中的Foobar)開(kāi)始執(zhí)行。這就是所謂的“棧分裂 (stack split)”。
+---------------+| || unused || stack || space | +---------------+ | Foobar | | | +---------------+ | | | lessstack | +---------------+ | Stack info | | |-----+ +---------------+ | | | +---------------+ | | Foobar | | | | <---+ +---------------+ | rest of stack | | |lessstack函數(shù)
在新棧的底部,插入了一個(gè)棧入口函數(shù)lessstack。設(shè)置這個(gè)函數(shù)用于從那個(gè)導(dǎo)致我們用光棧空間的函數(shù)(Foobar)返回時(shí)用的。當(dāng)那個(gè)函數(shù)(Foobar)返回時(shí),我們回到lessstack(這個(gè)棧幀),lessstack會(huì)查找 stack底部的那個(gè)struct,并調(diào)整棧指針(stack pointer),使得我們返回到前一段棧空間。這樣做之后,我們就可以將這個(gè)新棧段(stack segment)釋放掉,并繼續(xù)執(zhí)行我們的程序了。
分段棧的問(wèn)題
棧縮小是一個(gè)相對(duì)代價(jià)高昂的操作。如果在一個(gè)循環(huán)中調(diào)用的函數(shù)遇到棧分裂 (stack split),進(jìn)入函數(shù)時(shí)會(huì)增加棧空間(morestack 函數(shù)),返回并釋放棧段(lessstack 函數(shù))。性能方面開(kāi)銷(xiāo)很大。
連續(xù)棧(continuous stacks)
go現(xiàn)在使用的是這套解決方案。
goroutine在棧上運(yùn)行著,當(dāng)用光棧空間,它遇到與舊方案中相同的棧溢出檢查。但是與舊方案采用的保留一個(gè)返 回前一段棧的link不同,新方案創(chuàng)建一個(gè)兩倍于原stack大小的新stack,并將舊棧拷貝到其中。
這意味著當(dāng)棧實(shí)際使用的空間縮小為原先的 大小時(shí),go運(yùn)行時(shí)不用做任何事情。
棧縮小是一個(gè)無(wú)任何代價(jià)的操作(棧的收縮是垃圾回收的過(guò)程中實(shí)現(xiàn)的.當(dāng)檢測(cè)到棧只使用了不到1/4時(shí),棧縮小為原來(lái)的1/2)。
此外,當(dāng)棧再次增長(zhǎng)時(shí),運(yùn)行時(shí)也無(wú)需做任何事情,我們只需要重用之前分配的空閑空間即可。
如何捕獲到函數(shù)的棧空間不足
Go語(yǔ)言和C不同,不是使用棧指針寄存器和棧基址寄存器確定函數(shù)的棧的。
在Go的運(yùn)行時(shí)庫(kù)中,每個(gè)goroutine對(duì)應(yīng)一個(gè)結(jié)構(gòu)體G,大致相當(dāng)于進(jìn)程控制塊的概念。這個(gè)結(jié)構(gòu)體中存了stackbase和stackguard,用于確定這個(gè)goroutine使用的棧空間信息。每個(gè)Go函數(shù)調(diào)用的前幾條指令,先比較棧指針寄存器跟g->stackguard,檢測(cè)是否發(fā)生棧溢出。如果棧指針寄存器值超越了stackguard就需要擴(kuò)展棧空間。
舊棧數(shù)據(jù)復(fù)制到新棧
舊棧數(shù)據(jù)復(fù)制到新棧的過(guò)程,要考慮指針失效問(wèn)題。
Go實(shí)現(xiàn)了精確的垃圾回收,運(yùn)行時(shí)知道每一塊內(nèi)存對(duì)應(yīng)的對(duì)象的類(lèi)型信息。在復(fù)制之后,會(huì)進(jìn)行指針的調(diào)整。具體做法是,對(duì)當(dāng)前棧幀之前的每一個(gè)棧幀,對(duì)其中的每一個(gè)指針,檢測(cè)指針指向的地址,如果指向地址是落在舊棧范圍內(nèi)的,則將它加上一個(gè)偏移使它指向新棧的相應(yīng)地址。這個(gè)偏移值等于新棧基地址減舊棧基地址。
鏈接:https://www.jianshu.com/p/7ec9acca6480
轉(zhuǎn)載于:https://www.cnblogs.com/mafeng/p/10305419.html
總結(jié)
以上是生活随笔為你收集整理的深入理解golang 的栈的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: MySQL源码编译与初始化
- 下一篇: 远控免杀专题(10)-TheFatRat