# 6.7 執行棧管理
## 6.7.1 Goroutine 棧結構
Goroutine 是一個 g 對象,g 對象的前三個字段描述了它的執行棧:
```
// stack 描述了 Goroutine 的執行棧,棧的區間為 [lo, hi),在棧兩邊沒有任何隱式數據結構
// 因此 Go 的執行棧由運行時管理,本質上分配在堆中,比 ulimit -s 大
type stack struct {
lo uintptr
hi uintptr
}
// gobuf 描述了 Goroutine 的執行現場
type gobuf struct {
sp uintptr
pc uintptr
g guintptr
ctxt unsafe.Pointer
ret sys.Uintreg
lr uintptr
bp uintptr
}
type g struct {
// stack 描述了實際的棧內存:[stack.lo, stack.hi)
stack stack
// stackguard0 是對比 Go 棧增長的 prologue 的棧指針
// 如果 sp 寄存器比 stackguard0 小(由于棧往低地址方向增長),會觸發棧拷貝和調度
// 通常情況下:stackguard0 = stack.lo + StackGuard,但被搶占時會變為 StackPreempt
stackguard0 uintptr
// stackguard1 是對比 C 棧增長的 prologue 的棧指針
// 當位于 g0 和 gsignal 棧上時,值為 stack.lo + StackGuard
// 在其他棧上值為 ~0 用于觸發 morestackc (并 crash) 調用
stackguard1 uintptr
...
// sched 描述了執行現場
sched gobuf
...
}
```
~~~
<-- _StackPreempt
高地址
Goroutine stack
+-------------------+ <-- _g_.stack.hi
| |
+-------------------+
| |
+-------------------+
| |
+-------------------+ <-- _g_.sched.sp
| |
+-------------------+
| |
+-------------------+
| |
+-------------------+
| |
+-------------------+
....
| |
+-------------------+ <-- _g_.stackguard0
| | | |
+-------------------+ | | _StackSmall
| | | |
+-------------------+ | ---
| | |
+-------------------+ | _StackGuard
| | |
+-------------------+ <-- _g_.stack.lo
低地址
~~~
## 6.7.2 執行棧初始化
執行棧可以在函數執行完畢后,專門被垃圾回收整個回收掉,從而將它們單獨管理起來能夠利于垃圾回收器的統一回收:
```
// 具有可用棧的 span 的全局池
// 每個棧均根據其大小會被分配一個 order = log_2(size/FixedStack)
// 每個 order 都包含一個可用 mspan 鏈表
var stackpool [_NumStackOrders]struct {
item stackpoolItem
_ [cpu.CacheLinePadSize - unsafe.Sizeof(stackpoolItem{})%cpu.CacheLinePadSize]byte
}
//go:notinheap
type stackpoolItem struct {
mu mutex
span mSpanList
}
var stackLarge struct {
lock mutex
free [heapAddrBits - pageShift]mSpanList // 按 log_2(s.npages) 階組成的多個鏈表
}
```
`stackpool/stackLarge`均為全局變量,他們均為`mspan`的雙向鏈表,他們的初始化邏輯非常簡單, 既將整個鏈表初始化為空鏈,不分配節點:
```
//go:notinheap
type mSpanList struct { // 不帶頭結點的 mspan 雙向鏈表
first *mspan
last *mspan
}
func (list *mSpanList) init() {
list.first = nil
list.last = nil
}
```
`stackpool`和`stackLarge`的初始化僅僅就是將這兩個鏈表中不同階的 mspan 鏈表進行初始化:
```
func stackinit() {
...
for i := range stackpool {
stackpool[i].item.span.init()
}
for i := range stackLarge.free {
stackLarge.free[i].init()
}
}
```
## 6.7.3 G 的創生
一個 Goroutine 的創建通過`newproc`來完成,在調用這個函數之前,Goroutine 還尚未存在, 只有一個入口地址及參數的大小,我們通過下面的例子來理解:
```
package main
func hello(msg string) {
println(msg)
}
func main() {
go hello("hello world") // 7-8 行
}
```
其編譯后的形式為:
```
TEXT main.main(SB) main.go
main.go:7 0x104df70 65488b0c2530000000 MOVQ GS:0x30, CX
...
main.go:8 0x104df8d 488d055ed10100 LEAQ go.string.*+1874(SB), AX
main.go:8 0x104df94 4889442410 MOVQ AX, 0x10(SP)
main.go:8 0x104df99 48c74424180b000000 MOVQ $0xb, 0x18(SP)
main.go:8 0x104dfa2 c7042410000000 MOVL $0x10, 0(SP)
main.go:8 0x104dfa9 488d05b80c0200 LEAQ go.func.*+67(SB), AX
main.go:8 0x104dfb0 4889442408 MOVQ AX, 0x8(SP)
main.go:8 0x104dfb5 e876cefdff CALL runtime.newproc(SB)
...
```
具體的傳參過程:
```
LEAQ go.string.*+1874(SB), AX // 將 "hello world" 的地址給 AX
MOVQ AX, 0x10(SP) // 將 AX 的值放到 0x10
MOVL $0x10, 0(SP) // 將最后一個參數的位置存到棧頂 0x00
LEAQ go.func.*+67(SB), AX // 將 go 語句調用的函數入口地址給 AX
MOVQ AX, 0x8(SP) // 將 AX 存入 0x08
CALL runtime.newproc(SB) // 調用 newproc
```
這個過程里我們基本上可以看到棧是這樣排布的:
~~~
棧布局
| | 高地址
| |
+-----------------+
| &"hello world" |
0x10 +-----------------+ <-- fn + sys.PtrSize
| hello |
0x08 +-----------------+ <-- fn
| siz |
0x00 +-----------------+ <-- SP
| newproc PC |
+-----------------+ callerpc: 要運行的 Goroutine 的 PC
| |
| | 低地址
~~~
```
func newproc(siz int32, fn *funcval) {
// 從 fn 的地址增加一個指針的長度,從而獲取第一參數地址
argp := add(unsafe.Pointer(&fn), sys.PtrSize)
gp := getg()
// 獲取調用方 PC/IP 寄存器值
pc := getcallerpc()
// 用 g0 系統棧創建 Goroutine 對象
// 傳遞的參數包括 fn 函數入口地址, argp 參數起始地址, siz 參數長度, gp(g0),調用方 pc(Goroutine)
systemstack(func() {
newproc1(fn, (*uint8)(argp), siz, gp, pc)
})
}
```
當調用`newproc1`,會嘗試獲取一個已經分配好的 g,否則會直接進入創建:
```
func newproc1(fn *funcval, argp *uint8, narg int32, callergp *g, callerpc uintptr) {
...
newg := gfget(_p_) // 根據 p 獲得一個新的 g
// 初始化階段,gfget 是不可能找到 g 的
// 也可能運行中本來就已經耗盡了
if newg == nil {
newg = malg(_StackMin) // 創建一個擁有 _StackMin 大小的棧的 g
casgstatus(newg, _Gidle, _Gdead) // 將新創建的 g 從 _Gidle 更新為 _Gdead 狀態
allgadd(newg) // 將 Gdead 狀態的 g 添加到 allg,這樣 GC 不會掃描未初始化的棧
}
...
}
```
從而通過`malg`分配一個具有最小棧的 Goroutine:
```
// 分配一個新的 g 結構, 包含一個 stacksize 字節的的棧
func malg(stacksize int32) *g {
newg := new(g)
if stacksize >= 0 {
// 將 stacksize 舍入為 2 的指數,目的是為了消除 _StackSystem 對棧的影響
// 在 Linux/Darwin 上( _StackSystem == 0 )本行不改變 stacksize 的大小
stacksize = round2(_StackSystem + stacksize)
systemstack(func() {
newg.stack = stackalloc(uint32(stacksize))
})
newg.stackguard0 = newg.stack.lo + _StackGuard
newg.stackguard1 = ^uintptr(0)
}
return newg
}
```
`stackguard0`不出所料的被設置為了`stack.lo + _StackGuard`,而`stackguard1`則為`~0`。 而執行棧本身是通過`stackalloc`來進行分配。
## 6.7.4 執行棧的分配
前面已經提到棧可能從兩個不同的位置被分配:小棧和大棧。小棧指大小為 2K/4K/8K/16K 的棧,大棧則是更大的棧。`stackalloc`基本上也就是在權衡應該從哪里分配出一個執行棧,返回所在棧的低位和高位。 當然,高低位的確立很簡單,因為我們已經知道了需要棧的大小,那么只需要知道分配好的棧的起始位置在哪兒就夠了, 即指針`v`:
```
//go:systemstack
func stackalloc(n uint32) stack {
thisg := getg()
...
// 小棧由自由表分配器分配有固定大小。
// 如果我們需要更大尺寸的棧,我們將重新分配專用 span。
var v unsafe.Pointer
// 檢查是否從緩存分配
if n < _FixedStack<<_NumStackOrders && n < _StackCacheSize {
... // 小棧分配
} else {
... // 大棧分配
}
...
return stack{uintptr(v), uintptr(v) + uintptr(n)}
}
```
### 小棧分配
對于大小較小的棧可以從 stackpool 或者 stackcache 中進行分配,這取決于 當產生棧分配時,Goroutine 所在的 m 是否具有 mcache (`m.mcache`)或者是否發生搶占(`m.preemptoff`):
```
// 計算對應的 mSpanList
order := uint8(0)
n2 := n
for n2 > _FixedStack {
order++
n2 >>= 1
}
var x gclinkptr
c := thisg.m.mcache
// 決定是否從 stackpool 中分配
if c == nil || thisg.m.preemptoff != "" {
// c == nil 可能發生在 exitsyscall 或 procresize 時
lock(&stackpool[order].item.mu)
x = stackpoolalloc(order)
unlock(&stackpool[order].item.mu)
} else { // 從對應鏈表提取可復用的空間
x = c.stackcache[order].list
if x.ptr() == nil { // 提取失敗,擴容再重試
stackcacherefill(c, order)
x = c.stackcache[order].list
}
c.stackcache[order].list = x.ptr().next
c.stackcache[order].size -= uintptr(n)
}
v = unsafe.Pointer(x) // 最終取得 stack
```
如果沒多的緩存,則向內部填充更多的緩存:
```
//go:systemstack
func stackcacherefill(c *mcache, order uint8) {
...
// 從全局緩存中獲取一些 stack
// 獲取所允許的容量的一半來防止 thrashing
var list gclinkptr
var size uintptr
lock(&stackpool[order].item.mu)
for size < _StackCacheSize/2 {
x := stackpoolalloc(order)
x.ptr().next = list
list = x
size += _FixedStack << order
}
unlock(&stackpool[order].item.mu)
c.stackcache[order].list = list
c.stackcache[order].size = size
}
```
最終落實到`stackpoolalloc`上:
```
// 從空閑池中分配一個棧,必須在持有 stackpool[order].item.mu 下調用
func stackpoolalloc(order uint8) gclinkptr {
list := &stackpool[order].item.span
s := list.first // 鏈表頭
if s == nil {
// 緩存已空,從 mheap 上進行分配
s = mheap_.allocManual(_StackCacheSize>>_PageShift, &memstats.stacks_inuse)
...
s.elemsize = _FixedStack << order
for i := uintptr(0); i < _StackCacheSize; i += s.elemsize {
x := gclinkptr(s.base() + i)
x.ptr().next = s.manualFreeList
s.manualFreeList = x
}
list.insert(s)
}
x := s.manualFreeList
...
s.manualFreeList = x.ptr().next
s.allocCount++
if s.manualFreeList.ptr() == nil {
// s 中所有的棧都被分配了
list.remove(s)
}
return x
}
```
### 大棧分配
大空間從`stackLarge`進行分配:
```
var s *mspan
npage := uintptr(n) >> _PageShift
log2npage := stacklog2(npage)
// 嘗試從 stackLarge 緩存中獲取堆棧。
lock(&stackLarge.lock)
if !stackLarge.free[log2npage].isEmpty() {
s = stackLarge.free[log2npage].first
stackLarge.free[log2npage].remove(s)
}
unlock(&stackLarge.lock)
if s == nil { // 如果無法從緩存中獲取,則從堆中分配一個新的棧
s = mheap_.allocManual(npage, &memstats.stacks_inuse)
...
s.elemsize = uintptr(n)
}
v = unsafe.Pointer(s.base())
```
### 堆上分配
無論是小棧分配還是大棧的分配,在分配失敗時都會從`mheap`上分配重新分配新的緩存,使用`allocManual`:
```
//go:systemstack
func (h *mheap) allocManual(npage uintptr, stat *uint64) *mspan {
lock(&h.lock)
s := h.allocSpanLocked(npage, stat)
if s != nil {
s.state = mSpanManual
s.manualFreeList = 0
s.allocCount = 0
s.spanclass = 0
s.nelems = 0
s.elemsize = 0
s.limit = s.base() + s.npages<<_PageShift
...
}
// This unlock acts as a release barrier. See mheap.alloc_m.
unlock(&h.lock)
return s
}
```
其中的`allocSpanLocked`:
```
func (h *mheap) allocSpanLocked(npage uintptr, stat *uint64) *mspan {
t := h.free.find(npage) // 第一次從 mheap 的緩存中尋找
if t.valid() {
goto HaveSpan
}
if !h.grow(npage) { // 第一次沒找到,嘗試對堆進行擴充
return nil
}
t = h.free.find(npage) // 第二次從 mheap 緩存中尋找
if t.valid() {
goto HaveSpan
}
throw("grew heap, but no adequate free span found")
HaveSpan:
s := t.span()
...
return s
}
```
## 6.7.5 執行棧的伸縮
早年的 Go 運行時使用**分段棧**的機制,即當一個 Goroutine 的執行棧溢出時, 棧的擴張操作是在另一個棧上進行的,這兩個棧彼此沒有連續。 這種設計的缺陷很容易破壞緩存的局部性原理,從而降低程序的運行時性能。 因此現在 Go 運行時開始使用**連續棧**機制,當一個執行棧發生溢出時, 新建一個兩倍于原棧大小的新棧,再將原棧整個拷貝到新棧上。 從而整個棧總是連續的。棧的拷貝并非想象中的那樣簡單,因為一個棧上可能保留指向被拷貝棧的指針, 從而當棧發生拷貝后,這個指針可能還指向原棧,從而造成錯誤。 此外,Goroutine 上原本的`gobuf`也需要被更新,這也是使用連續棧的難點之一。
### 分段標記
分段標記是編譯器的機制,涉及棧幀大小的計算。這個過程比較復雜,我們暫時假設編譯器已經計算好了棧幀的大小, 這時,編譯的預處理階段,會為沒有標記為`go:nosplit`的函數插入棧的分段檢查:
```
// cmd/internal/obj/x86/obj6.go
func preprocess(ctxt *obj.Link, cursym *obj.LSym, newprog obj.ProgAlloc) {
...
p := cursym.Func.Text
autoffset := int32(p.To.Offset) // 棧幀大小
// 一些額外的棧幀大小計算
...
if !cursym.Func.Text.From.Sym.NoSplit() {
p = stacksplit(ctxt, cursym, p, newprog, autoffset, int32(textarg)) // 觸發分段檢查
}
...
}
```
與處理階段將棧幀大小傳入`stacksplit`,用于針對不同大小的棧進行不同的分段檢查, 具體的代碼相當繁瑣,這里直接給出的是匯編的偽代碼:
```
func stacksplit(ctxt *obj.Link, cursym *obj.LSym, p *obj.Prog, newprog obj.ProgAlloc, framesize int32, textarg int32) *obj.Prog {
...
var q1 *obj.Prog
if framesize <= objabi.StackSmall {
// 小棧: SP <= stackguard,直接比較 SP 和 stackguard
// CMPQ SP, stackguard
...
} else if framesize <= objabi.StackBig {
// 大棧: SP-framesize <= stackguard-StackSmall
// LEAQ -xxx(SP), AX
// CMPQ AX, stackguard
...
} else {
// 更大的棧需要防止 wraparound
// 如果 SP 接近于零:
// SP-stackguard+StackGuard <= framesize + (StackGuard-StackSmall)
// 兩端的 +StackGuard 是為了保證左側大于零。
// SP 允許位于 stackguard 下面一點點
//
// 搶占設置了 stackguard 為 StackPreempt,一個大到能夠打破上面的數學計算的值,
// 因此必須顯式的進行檢查:
// MOVQ stackguard, CX
// CMPQ CX, $StackPreempt
// JEQ label-of-call-to-morestack
// LEAQ StackGuard(SP), AX
// SUBQ CX, AX
// CMPQ AX, $(framesize+(StackGuard-StackSmall))
...
}
...
// 函數的尾聲
morestack := "runtime.morestack"
switch {
case cursym.CFunc():
morestack = "runtime.morestackc" // morestackc 會 panic,因為此時是系統棧上的 C 函數
case !cursym.Func.Text.From.Sym.NeedCtxt():
morestack = "runtime.morestack_noctxt"
}
call.To.Sym = ctxt.Lookup(morestack)
...
return jls
}
```
總而言之,沒有被`go:nosplit`標記的函數的序言部分會插入分段檢查,從而在發生棧溢出的情況下, 觸發`runtime.morestack`調用,如果函數不需要`ctxt`,則會調用`runtime.morestack_noctxt`從而拋棄`ctxt`再調用`morestack`:
```
TEXT runtime·morestack_noctxt(SB),NOSPLIT,$0
MOVL $0, DX
JMP runtime·morestack(SB)
```
### 棧的擴張
用戶棧的擴張發生在 morestack 處,該函數此前會檢查該調用是否正確的在用戶棧上調用(因此 g0 棧和信號棧 不能發生此調用)。而后將`morebuf`設置為 f 的調用方,并將 G 的執行棧設置為 f 的 ctxt, 從而在 g0 上調用`newstack`。
```
TEXT runtime·morestack(SB),NOSPLIT,$0-0
// 無法增長調度器的棧(m->g0)
get_tls(CX)
MOVQ g(CX), BX
MOVQ g_m(BX), BX
MOVQ m_g0(BX), SI
CMPQ g(CX), SI
JNE 3(PC)
CALL runtime·badmorestackg0(SB)
CALL runtime·abort(SB)
// 無法增長信號棧 (m->gsignal)
MOVQ m_gsignal(BX), SI
CMPQ g(CX), SI
JNE 3(PC)
CALL runtime·badmorestackgsignal(SB)
CALL runtime·abort(SB)
// 從 f 調用
// 將 m->morebuf 設置為 f 的調用方
MOVQ 8(SP), AX // f 的調用方 PC
MOVQ AX, (m_morebuf+gobuf_pc)(BX)
LEAQ 16(SP), AX // f 的調用方 SP
MOVQ AX, (m_morebuf+gobuf_sp)(BX)
get_tls(CX)
MOVQ g(CX), SI
MOVQ SI, (m_morebuf+gobuf_g)(BX)
// 將 g->sched 設置為 f 的 context
MOVQ 0(SP), AX // f 的 PC
MOVQ AX, (g_sched+gobuf_pc)(SI)
MOVQ SI, (g_sched+gobuf_g)(SI)
LEAQ 8(SP), AX // f 的 SP
MOVQ AX, (g_sched+gobuf_sp)(SI)
MOVQ BP, (g_sched+gobuf_bp)(SI)
MOVQ DX, (g_sched+gobuf_ctxt)(SI)
// 在 m->g0 棧上調用 newstack.
MOVQ m_g0(BX), BX
MOVQ BX, g(CX)
MOVQ (g_sched+gobuf_sp)(BX), SP
CALL runtime·newstack(SB)
CALL runtime·abort(SB) // 如果 newstack 返回則崩潰
RET
```
`newstack`在前半部分承擔了對 Goroutine 進行搶占的任務(見[6.8 協作與搶占](https://golang.design/under-the-hood/zh-cn/part2runtime/ch06sched/preemption)), 而在后半部分則是真正的棧擴張。
```
//go:nowritebarrierrec
func newstack() {
thisg := getg()
...
gp := thisg.m.curg
...
morebuf := thisg.m.morebuf
thisg.m.morebuf.pc = 0
thisg.m.morebuf.lr = 0
thisg.m.morebuf.sp = 0
thisg.m.morebuf.g = 0
...
sp := gp.sched.sp
if sys.ArchFamily == sys.AMD64 || sys.ArchFamily == sys.I386 || sys.ArchFamily == sys.WASM {
// 到 morestack 的調用會消耗一個字
sp -= sys.PtrSize
}
...
// 分配一個更大的段,并對棧進行移動
oldsize := gp.stack.hi - gp.stack.lo
newsize := oldsize * 2 // 兩倍于原來的大小
// 需要的棧太大,直接溢出
if newsize > maxstacksize {
print("runtime: goroutine stack exceeds ", maxstacksize, "-byte limit\n")
throw("stack overflow")
}
// goroutine 必須是正在執行過程中才來調用 newstack
// 所以這個狀態一定是 Grunning 或 Gscanrunning
casgstatus(gp, _Grunning, _Gcopystack)
// 因為 gp 處于 Gcopystack 狀態,當我們對棧進行復制時并發 GC 不會掃描此棧
copystack(gp, newsize, true)
...
casgstatus(gp, _Gcopystack, _Grunning)
gogo(&gp.sched) // 繼續執行
}
```
### 棧的拷貝
前面我們已經提到了,棧拷貝的其中一個難點就是 Go 中棧上的變量會包含自己的地址, 當我們拷貝了一個指向原棧的指針時,拷貝后的指針會變為無效指針。 不難發現,只有棧上分配的指針才能指向棧上的地址,否則這個指針指向的對象會重新在堆中進行分配(逃逸)。
```
func copystack(gp *g, newsize uintptr, sync bool) {
...
old := gp.stack
...
used := old.hi - gp.sched.sp
// 分配新的棧
new := stackalloc(uint32(newsize))
if stackPoisonCopy != 0 {
fillstack(new, 0xfd)
}
...
// 計算調整的幅度
var adjinfo adjustinfo
adjinfo.old = old
adjinfo.delta = new.hi - old.hi
// 調整 sudogs, 必要時與 channel 操作同步
ncopy := used
if sync {
adjustsudogs(gp, &adjinfo)
} else {
// sudogs can point in to the stack. During concurrent
// shrinking, these areas may be written to. Find the
// highest such pointer so we can handle everything
// there and below carefully. (This shouldn't be far
// from the bottom of the stack, so there's little
// cost in handling everything below it carefully.)
adjinfo.sghi = findsghi(gp, old)
// Synchronize with channel ops and copy the part of
// the stack they may interact with.
ncopy -= syncadjustsudogs(gp, used, &adjinfo)
}
// 將原來的棧的內容復制到新的位置
memmove(unsafe.Pointer(new.hi-ncopy), unsafe.Pointer(old.hi-ncopy), ncopy)
// Adjust remaining structures that have pointers into stacks.
// We have to do most of these before we traceback the new
// stack because gentraceback uses them.
adjustctxt(gp, &adjinfo)
adjustdefers(gp, &adjinfo)
adjustpanics(gp, &adjinfo)
if adjinfo.sghi != 0 {
adjinfo.sghi += adjinfo.delta
}
// 為新棧置換出舊棧
gp.stack = new
gp.stackguard0 = new.lo + _StackGuard // 注意: 可能覆蓋(clobber)一個搶占請求
gp.sched.sp = new.hi - used
gp.stktopsp += adjinfo.delta
// 在新棧重調整指針
gentraceback(^uintptr(0), ^uintptr(0), 0, gp, 0, nil, 0x7fffffff, adjustframe, noescape(unsafe.Pointer(&adjinfo)), 0)
// 釋放舊棧
if stackPoisonCopy != 0 {
fillstack(old, 0xfc)
}
stackfree(old)
}
func fillstack(stk stack, b byte) {
for p := stk.lo; p < stk.hi; p++ {
*(*byte)(unsafe.Pointer(p)) = b
}
}
func findsghi(gp *g, stk stack) uintptr {
var sghi uintptr
for sg := gp.waiting; sg != nil; sg = sg.waitlink {
p := uintptr(sg.elem) + uintptr(sg.c.elemsize)
if stk.lo <= p && p < stk.hi && p > sghi {
sghi = p
}
}
return sghi
}
func syncadjustsudogs(gp *g, used uintptr, adjinfo *adjustinfo) uintptr {
if gp.waiting == nil {
return 0
}
// Lock channels to prevent concurrent send/receive.
// It's important that we *only* do this for async
// copystack; otherwise, gp may be in the middle of
// putting itself on wait queues and this would
// self-deadlock.
var lastc *hchan
for sg := gp.waiting; sg != nil; sg = sg.waitlink {
if sg.c != lastc {
lock(&sg.c.lock)
}
lastc = sg.c
}
// Adjust sudogs.
adjustsudogs(gp, adjinfo)
// Copy the part of the stack the sudogs point in to
// while holding the lock to prevent races on
// send/receive slots.
var sgsize uintptr
if adjinfo.sghi != 0 {
oldBot := adjinfo.old.hi - used
newBot := oldBot + adjinfo.delta
sgsize = adjinfo.sghi - oldBot
memmove(unsafe.Pointer(newBot), unsafe.Pointer(oldBot), sgsize)
}
// Unlock channels.
lastc = nil
for sg := gp.waiting; sg != nil; sg = sg.waitlink {
if sg.c != lastc {
unlock(&sg.c.lock)
}
lastc = sg.c
}
return sgsize
}
```
### 棧的收縮
棧的收縮發生在 GC 時對棧進行掃描的階段:
```
//go:nowritebarrier
//go:systemstack
func scanstack(gp *g, gcw *gcWork) {
...
// _Grunnable, _Gsyscall, _Gwaiting 才會發生
// 如果棧使用不多,則進行棧收縮
shrinkstack(gp)
...
}
func shrinkstack(gp *g) {
...
oldsize := gp.stack.hi - gp.stack.lo
newsize := oldsize / 2
// 當收縮后的大小小于最小的棧的大小時,不再進行收縮
if newsize < _FixedStack {
return
}
// 計算當前正在使用的棧數量,如果 gp 使用的當前棧少于四分之一,則對棧進行收縮。
// 當前使用的棧包括到 SP 的所有內容以及棧保護空間,以確保有 nosplit 功能的空間。
avail := gp.stack.hi - gp.stack.lo
if used := gp.stack.hi - gp.sched.sp + _StackLimit; used >= avail/4 {
return
}
// 在系統調用期間無法對棧進行拷貝
// 因為系統調用可能包含指向棧的指針
if gp.syscallsp != 0 {
return
}
if sys.GoosWindows != 0 && gp.m != nil && gp.m.libcallsp != 0 {
return
}
...
// 將舊棧拷貝到新收縮后的棧上
copystack(gp, newsize, false)
}
```
可以看到,如果一個棧僅被使用了四分之一,則會觸發棧的收縮,收縮后的大小是原來棧大小的一半。
- 第一部分 :基礎篇
- 第1章 Go語言的前世今生
- 1.2 Go語言綜述
- 1.3 順序進程通訊
- 1.4 Plan9匯編語言
- 第2章 程序生命周期
- 2.1 從go命令談起
- 2.2 Go程序編譯流程
- 2.3 Go 程序啟動引導
- 2.4 主Goroutine的生與死
- 第3 章 語言核心
- 3.1 數組.切片與字符串
- 3.2 散列表
- 3.3 函數調用
- 3.4 延遲語句
- 3.5 恐慌與恢復內建函數
- 3.6 通信原語
- 3.7 接口
- 3.8 運行時類型系統
- 3.9 類型別名
- 3.10 進一步閱讀的參考文獻
- 第4章 錯誤
- 4.1 問題的演化
- 4.2 錯誤值檢查
- 4.3 錯誤格式與上下文
- 4.4 錯誤語義
- 4.5 錯誤處理的未來
- 4.6 進一步閱讀的參考文獻
- 第5章 同步模式
- 5.1 共享內存式同步模式
- 5.2 互斥鎖
- 5.3 原子操作
- 5.4 條件變量
- 5.5 同步組
- 5.6 緩存池
- 5.7 并發安全散列表
- 5.8 上下文
- 5.9 內存一致模型
- 5.10 進一步閱讀的文獻參考
- 第二部分 運行時篇
- 第6章 并發調度
- 6.1 隨機調度的基本概念
- 6.2 工作竊取式調度
- 6.3 MPG模型與并發調度單
- 6.4 調度循環
- 6.5 線程管理
- 6.6 信號處理機制
- 6.7 執行棧管理
- 6.8 協作與搶占
- 6.9 系統監控
- 6.10 網絡輪詢器
- 6.11 計時器
- 6.12 非均勻訪存下的調度模型
- 6.13 進一步閱讀的參考文獻
- 第7章 內存分配
- 7.1 設計原則
- 7.2 組件
- 7.3 初始化
- 7.4 大對象分配
- 7.5 小對象分配
- 7.6 微對象分配
- 7.7 頁分配器
- 7.8 內存統計
- 第8章 垃圾回收
- 8.1 垃圾回收的基本想法
- 8.2 寫屏幕技術
- 8.3 調步模型與強弱觸發邊界
- 8.4 掃描標記與標記輔助
- 8.5 免清掃式位圖技術
- 8.6 前進保障與終止檢測
- 8.7 安全點分析
- 8.8 分代假設與代際回收
- 8.9 請求假設與實務制導回收
- 8.10 終結器
- 8.11 過去,現在與未來
- 8.12 垃圾回收統一理論
- 8.13 進一步閱讀的參考文獻
- 第三部分 工具鏈篇
- 第9章 代碼分析
- 9.1 死鎖檢測
- 9.2 競爭檢測
- 9.3 性能追蹤
- 9.4 代碼測試
- 9.5 基準測試
- 9.6 運行時統計量
- 9.7 語言服務協議
- 第10章 依賴管理
- 10.1 依賴管理的難點
- 10.2 語義化版本管理
- 10.3 最小版本選擇算法
- 10.4 Vgo 與dep之爭
- 第12章 泛型
- 12.1 泛型設計的演進
- 12.2 基于合約的泛型
- 12.3 類型檢查技術
- 12.4 泛型的未來
- 12.5 進一步閱讀的的參考文獻
- 第13章 編譯技術
- 13.1 詞法與文法
- 13.2 中間表示
- 13.3 優化器
- 13.4 指針檢查器
- 13.5 逃逸分析
- 13.6 自舉
- 13.7 鏈接器
- 13.8 匯編器
- 13.9 調用規約
- 13.10 cgo與系統調用
- 結束語: Go去向何方?