前言
本文拉開垃圾回收部分序幕(預告:會切入一些關鍵點分析,杜絕市面千篇一律的內容)。由於Go協程的棧是Go運行時管理的,並分配於堆上,不由操作系統管理,所以我們先來看看協程棧的內存如何被Go運行管理和回收的。本篇文章先從初步認識協程棧開始。
為了對協程棧有個初步的認識,我們先來回顧數據結構中棧的概念,再來看看內存棧的概念作用,最後我們再來通過對比進程中的棧內存和線程中的棧內存來對協程中的棧內存有個初步的認知,全文大致結構如下:
- 回顧數據結構中棧的概念
- 內存管理中棧的概念
- 理解進程棧和線程棧
- 認識協程棧
數據結構中棧的概念
棧是一種先進後出(Frist In Last Out)的數據結構。第一個元素所在位置為棧底,最後一個元素所在位置為棧頂。棧頂添加一個元素的過程為壓棧(入棧),棧頂移出一個元素的過程為出棧(彈棧)。如下圖所示:
內存管理中棧的概念
棧內存
什麼是棧內存?
棧內存是計算機對連續內存的採取的「線性分配」管理方式,便於高效存儲指令運行過程中的臨時變量等。
- 棧內存分配邏輯:current - alloc
- 棧內存釋放邏輯:current + alloc
- 棧內存的分配過程:看起來像不像數據結構「棧」的壓棧過程。
- 棧內存的釋放過程:看起來像不像數據結構「棧」的出棧過程。
什麼是棧空間?
棧空間是一個固定值,決定了棧內存最大可分配的內存範圍,也就是決定了棧頂的上限。
棧內存的作用?
總的來説就是存放程序運行過程中,指令傳輸的、生產的各種臨時變量的值,和函數遞歸調用過程的別要信息,以及進程、線程、協程切換的上下文信息。
- 存放函數執行過程中的參數的值
- 存放函數執行過程中的局部變量的值
- 發生函數調用時,存放調用函數棧幀的棧基BP的值(下篇文章詳細講)
- 發生函數調用時,存放調用函數下一個待執行指令的地址(下篇文章詳細講)
- 等等
接着我有兩個問題:
- 誰決定了棧空間的大小範圍?
- 誰決定了代碼在運行過程中,從棧空間分配或釋放多少內存?
我們分別從「進程棧」和「線程棧」、「協程棧」視角看看以上兩個問題。
進程棧
什麼是進程棧?
答:位於進程虛擬內存的用户空間,以用户空間的高地址開始位置作為棧底,向地址分配。如下圖所示:
誰決定了棧空間(進程棧)的大小範圍?
答:操作系統的配置決定,可通過`ulimit -s`查看。示例如下:
啊
(TIGERB) 🤔 ➜ go1.16 git:((go1.16)) ✗ ulimit -s
8192 //註釋:單位kb,8m
(TIGERB) 🤔 ➜ go1.16 git:((go1.16)) ✗ ulimit -a
-t: cpu time (seconds) unlimited
-f: file size (blocks) unlimited
-d: data seg size (kbytes) unlimited
-s: stack size (kbytes) 8192 //註釋:單位kb,8m
-c: core file size (blocks) 0
-v: address space (kbytes) unlimited
-l: locked-in-memory size (kbytes) unlimited
-u: processes 1392
-n: file descriptors 256
誰決定了代碼在運行過程中,從棧空間(進程棧)分配或釋放多少內存?
答:編譯器決定。詳細解釋如下:
代碼編譯時,編譯器會插入分配和釋放棧內存的指令,比如像下面這段簡單的程序一樣:
一段簡單的加法示例代碼:
// 源代碼
package main
func main() {
a := 1
b := 2
c := a + b
// 略...
}
編譯以上代碼時,編譯器會插入分配(SUB)和釋放(ADD)棧內存的指令:
// 彙編偽代碼
SUB 24, SP // 棧上分配24字節內存 3*8byte(分配棧內存指令)
略...
ADD 24, SP // 棧上釋放24字節內存 3*8byte(釋放棧內存指令)
略...
最後彙編代碼轉換為二進制代碼:
// 二進制偽代碼 隨便亂寫的
011100011000000101...
進程棧總結
「進程棧」位於虛擬內存的用户空間,進程棧的棧底為用户空間部分高地址的開始位置。進程棧的棧空間大小為固定值,由操作系統的配置決定。進程運行過程中棧內存的分配和釋放的時機和大小值由編譯器決定。
線程棧
什麼是線程棧?
答:創建一個線程時,使用malloc從堆上分配一塊連續內存作為線程的棧空間。
偽代碼示例:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define STACK_SIZE 1024 * 1024 // 棧空間大小
int main() {
pthread_t thread;
void* stack = malloc(STACK_SIZE); // 堆上申請一塊內存
// ...
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setstack(&attr, stack, STACK_SIZE); // 設置線程棧
int ret = pthread_create(&thread, &attr, thread_func, NULL); // 創建線程
// ...
}
誰決定了棧空間(線程棧)的大小範圍?
答:創建線程的運行時。
誰決定了代碼在運行過程中,從棧空間(線程棧)分配或釋放多少內存?
答:同進程,編譯器決定。
協程棧
什麼是協程棧?
答:使用`go`關鍵自創建一個協程時,Go運行時從堆上分配一塊連續內存作為協程的棧空間。
誰決定了協程棧的棧空間的大小範圍?
答:Go運行時決定,g0為8KB,g為2KB
創建g0函數代碼片段:
// src/runtime/proc.go::1720
// 創建 m
func allocm(_p_ *p, fn func(), id int64) *m {
// ...略
if iscgo || mStackIsSystemAllocated() {
mp.g0 = malg(-1)
} else {
// 創建g0 並申請8KB棧內存
// 依賴的malg函數
mp.g0 = malg(8192 * sys.StackGuardMultiplier)
}
// ...略
}
創建g函數代碼片段:
// src/runtime/proc.go::3999
// 創建一個帶有任務fn的goroutine
func newproc1(fn *funcval, argp unsafe.Pointer, narg int32, callergp *g, callerpc uintptr) *g {
// ...略
newg := gfget(_p_)
if newg == nil {
// 全局隊列、本地隊列找不到g 則 創建一個全新的goroutine
// _StackMin = 2048
// 申請2KB棧內存
// 依賴的malg函數
newg = malg(_StackMin)
casgstatus(newg, _Gidle, _Gdead)
allgadd(newg)
}
// ...略
}
以上都依賴malg函數代碼片段,其作用是創建一個全新g:
// src/runtime/proc.go::3943
// 創建一個指定棧內存的g
func malg(stacksize int32) *g {
newg := new(g)
if stacksize >= 0 {
// ...略
systemstack(func() {
// 分配棧內存
newg.stack = stackalloc(uint32(stacksize))
})
// ...略
}
return newg
}
誰決定了代碼在運行過程中,從協程棧的棧空間分配或釋放多少內存?
答:同進程、線程,都由編譯器決定。
總結
| 類型 | 創建時機 | 誰決定棧空間大小 | 內存位置 | 誰來分配和釋放棧內存 |
|---|---|---|---|---|
| 進程棧 | 進程啓動時 | 操作系統配置,ulimit -s |
虛擬內存的用户空間棧區 | 編譯器,彙編SUB、ADD指令 |
| 線程棧 | 創建線程時 | 創建線程的運行時,pthread_attr_setstack |
虛擬內存的用户空間進程堆區域 | 編譯器,彙編SUB、ADD指令 |
| 協程棧 | 使用go關鍵字運行函數時 |
Go運行時,malg(棧內存)g0 8KB、g 2KB |
虛擬內存的用户空間進程堆區域 | 編譯器,彙編SUB、ADD指令 |