go 1.12.7
文中未標明包名之名稱均在 runtime
包中
interface
type iface struct {
type eface struct {
- 非空接口類型
iface
結構體包含:tab *itab
data unsafe.Pointer
- 空接口類型(即
interface{}
類型)eface
結構體包含:_type *_type
data unsafe.Pointer
-
itab
結構體包含:-
hash uint32
用于在接口和具體類型轉換時判定類型是否相符 -
inter *interfacetype
接口類型的信息 -
_type *_type
具體類型的信息 -
fun [1]uintptr
用作虛表,直接使用該字段的指針,用作一個變長數組
-
- 具體類型轉為接口,
convT2I
函數:- 在編譯時就已構造好
*itab
結構 - 傳入
*itab
和具體類型的指針 - 分配具體類型大小的空間,將
data
字段指向空間,將具體類型拷貝到空間
- 在編譯時就已構造好
- 接口類型轉為接口,
convI2I
函數:- 傳入轉換后接口類型的
*interfacetype
和轉換前的iface
- 如果轉換后的
*interfacetype
和轉換前的iface.tab.inter
相等,則前后iface
的兩個字段直接拷貝 - 否則,檢查具體類型是否實現轉換后的接口,特別是構造虛表,此過程的結果會緩存
- 傳入轉換后接口類型的
slice
type slice struct {
- range 接收的是被 range 對象的按值傳遞,因此在一個對 slice 的 range 中對 slice 進行 append 不會造成無限循環
- 擴容,
growslice
函數:- 如果新長度大于當前容量的兩倍,就擴容至新長度
- 否則如果當前容量小于 1024,就將容量翻倍
- 否則,循環增加當前容量的 1/4 直到大于等于新長度
map
type hmap struct {
-
hmap
結構體包含:-
count int
鍵值對的個數 -
hash0 uint32
求哈希值的隨機種子 -
B uint8
桶的數量的以 2 為底的對數,桶的數量總是 2 的整次冪 -
buckets unsafe.Pointer
當前桶的指針,指向 malloc 出的連續多個桶的第一個 -
oldbuckets unsafe.Pointer
擴容之前的桶的指針
-
- 創建 map,
makemap
函數:根據make
函數傳入的 hint 來創建初始的 2 的整次冪個桶。桶的結構為bmap
,但bmap
真正的字段結構是在運行時創建的,而非代碼中聲明的結構,因為 key 有不同的類型,而 golang 沒有泛型!這也是為什么buckets
字段的類型為unsafe.Pointer
而非*bmap
- 邏輯上
bmap
結構體包含:-
topbits [8]uint8
緩存每個 key 的哈希值的高 8 位 keys [8]K
-
values [8]V
可見每個桶最多存 8 個鍵值對
-
- range 遍歷,
mapiterinit
函數:生成一個隨機數來決定從哪個桶開始遍歷 -
m[k]
式的讀操作,mapaccess1
函數:計算uintptr
類型的哈希值,通過哈希值的低B
位確定訪問哪個桶,通過哈希值的高 8 位與桶中的topbits
逐一對比,相同時再進行 key 的對比 - 寫操作,
mapassign
函數:與m[k]
式的讀相似地通過哈希值查找,如果發現 key 不存在則寫入到第一個空的點位。如果鍵值對數量與容量的比值大于 6.5/8 且未在擴容中則開始擴容 - 擴容,
hashGrow
函數:將buckets
賦給oldbuckets
,給buckets
分配翻倍的桶數,原第i
個桶的數據會遷移到新第i
和i + len(oldbuckets)
個桶。每個桶的數據遷移是在該桶涉及到寫和刪操作時進行的(growWork
函數)
func
- 與 C 語言使用寄存器不同,Golang 只使用棧進行函數參數和返回值的傳遞,被調函數的參數和返回值存放在主調函數的棧幀上,這也是支持多值返回的原因
- Plan9 寄存器:
- 通用寄存器
AX ~ DX, DI, SI, BP, SP, R8 ~ R14, PC
- 偽寄存器:
-
FP
:主調函數(上一個棧幀)中對當前函數調用的參數的起始(最低)地址,使用形式為symbol+offset(FP)
,如arg1+8(FP)
,offset
只是關于FP
的偏移,symbol
只是一個增強可讀性的標記 -
SP
:當前函數局部變量的起始(最高)地址,使用形式為symbol-offset(SP)
,offset
只是關于FP
的偏移,symbol
只是一個增強可讀性的標記。與通用寄存器SP
的區分方式為,不帶symbol+
形式的為通用寄存器 -
SB
:全局區起始(最低)地址,使用形式為symbol+offset(SB)
,offset
是關于symbol
的偏移
-
- 通用寄存器
- 棧幀結構(主調函數 -> 當前函數 -> 被調函數):
- 當前函數的棧幀從地址
b
開始向低地址發展 -
b-1
到b-8
存放主調函數的BP
,偽 SP
和 當前BP
指向b-8
-
b-9
到c
存放局部變量 var0 到 varN -
c
到d
存放被調函數的返回值 retN 到 ret0 和 參數 argN 到 arg0,偽 FP
和 當前SP
指向d
-
d-1
到d-8
存放被調函數返回時需要回到的PC
- 當前函數的棧幀從地址
defer
type _defer struct {
-
_defer
結構體包含:-
sp uintptr
當前函數的棧指針 -
pc uintptr
當前函數的程序指針 -
fn *funcval
傳給 defer 的函數
-
- defer 語句執行,
deferproc
函數:設置以上字段,傳遞 defer 函數的參數,將_defer
結構體置于當前協程的
_defer
結構體組成的鏈表的頭部 - 編譯時會插入代碼,在當前函數返回時,遍歷執行鏈表中所有棧指針與當前函數棧指針相同的
_defer
結構體中的函數(deferreturn
函數)
goroutine
type m struct {
type g struct {
type p struct {
- 操作系統線程
m
結構體包含:-
g0 *g
擁有調度棧的調度協程 -
curg *g
當前運行的協程 -
p uintptr
綁定的調度器
-
- 協程
g
結構體包含:-
m *m
綁定的線程 -
sched gobuf
寄存器等上下文 atomicstatus uint32
-
- 協程狀態
atomicstatus
:-
_Gidle
未初始化 -
_Gdead
未運行,不在隊列中 -
_Grunnable
未運行,在隊列中等待調度 -
_Grunning
正運行在用戶態,不在隊列中 -
_Gsyscall
正運行在內核態,不在隊列中 -
_Gwaiting
被阻塞,不在隊列中
-
- 調度器
p
結構包含:-
m uintptr
綁定的線程 -
runq [256]uintptr
待運行協程的隊列,數組用作一個循環隊列 -
runnext uintptr
下一個運行的協程結構體的指針 -
gFree ...
狀態為_Gdead
的空閑協程結構的隊列
-
-
GOMAXPROCS
個線程運行在用戶態,默認為 CPU 核數。一個線程綁定一個調度器。同時存在一個全局待運行協程隊列sched.runq
- go 關鍵字執行,
newproc
函數:- 從當前線程的調度器的空閑隊列中獲取一個協程結構,如果沒有就新建一個并分配棧空間
- 將入口函數的參數整片拷貝到新協程的棧中
- 新協程加入隊列,
runqput
函數:新協程的狀態置為_Grunnable
,特權式地添加到調度器中,協程的指針直接設置至runnext
字段。如果隊列已滿,將之前的runnext
發配到全局隊列
- 協程暫停,
gopark
函數:- 切換至調度協程
g0
,mcall
函數:匯編實現,保存當前協程程序指針、棧指針,設置 CPU 寄存切換至調度協程 - 處理當前協程,
park_m
函數:當前協程狀態置為_Gwaiting
- 選擇下一協程,
schedule
函數:- 如果需要執行 gc 標記任務,選擇一個當前線程調度器中的 gc 標記任務協程
- 否則一定幾率從全局隊列獲取
- 否則從當前線程的調度器獲取:如果
runnext
不為空,則選擇它,否則選擇隊列頭部 - 否則,調用
findrunnable
從其它調度器、全局隊列、epoll 中獲取,直到獲取到一個才會返回
- 執行下一協程,
execute
函數:狀態置為_Grunning
,建立與線程的關系,在匯編實現的gogo
函數中設置 CPU 寄存切換至下一協程
- 切換至調度協程
- 系統調用,
syscall.Syscall
函數:- 進入,
entersyscall
函數:保存當前的程序指針、棧指針,當前協程狀態置為_Gsyscall
,解除調度器與線程的綁定,線程陷入內核態 - 退出,
exitsyscall
函數:線程重新綁定調度器
- 進入,
channel/select
type hchan struct {
-
hchan
結構體包含:-
buf unsafe.Pointer
緩沖區隊列 -
qcount uint
緩沖區的長度 -
dataqsiz uint
緩沖區的容量,因為是一個循環隊列 -
sendx uint
寫到哪一個下標 -
recvx uint
讀到哪一個下標 -
elemtype *_type
緩沖區元素的類型信息 -
elemsize uint16
緩沖區元素的大小 -
sendq waitq
因為寫而阻塞于此的協程列表,元素類型為*sudog
,雙向鏈表 -
recvq waitq
因為讀而阻塞于此的協程列表
-
- 創建 channel,
makechan
函數:如果無緩沖器,則不為buf
分配空間;否則如果元素不為指針類型,則為buf
分配空間和hchan
連續的空間;否則為buf
分配獨立的空間 - 向 channel 發送,
chansend1
函數:- 如果 channel 為
nil
,則協程永遠阻塞 - 如果 channel 已關閉,則 panic
- 如果有因為讀而阻塞于此的協程,
send
函數:- 將接收方的
sudog
移出recvq
- 將消息拷貝在
sudog
中的接收變量的地址 - 將接收方協程的狀態從
_Gwaiting
置為_Grunnable
,同樣特權式地將協程插入到調度器的隊列 - 發送方協程不會阻塞,狀態始終是
_Grunning
- 將接收方的
- 否則如果 channel 有緩沖區且未滿,則將消息拷貝到緩沖區尾部
- 否則,阻塞發送:
- 將當前協程以及發送變量的指針存入
sudog
,將sudog
加入sendq
- 調用
gopark
暫停當前協程 - 等接收操作到來時,此協程會被重新調度
- 將當前協程以及發送變量的指針存入
- 如果 channel 為
- 從 channel 接收,
chanrecv1
函數:- 如果 channel 為
nil
,則協程永遠阻塞 - 如果 channel 已關閉且緩沖區為空,則將接收變量置零并返回
- 如果有因為寫而阻塞于此的協程,
recv
函數:- 將發送方的
sudog
移出sendq
- 將消息從在
sudog
中的發送變量的地址拷貝,如果有緩沖區且不空,則將消息拷貝到緩沖區尾部,將頭部出隊并拷貝到接收變量,否則將消息拷貝到接收變量 - 將發送方協程的狀態從
_Gwaiting
置為_Grunnable
,同樣特權式地將協程插入到調度器的隊列 - 接收方協程不會阻塞,狀態始終是
_Grunning
- 將發送方的
- 否則如果 channel 有緩沖區且不空,則將頭部出隊并拷貝到接收變量
- 否則,阻塞接收,過程與發送對偶
- 如果 channel 為
- 關閉 channel,
closechan
函數:- 如果 channel 為 nil 或已關閉,則 panic
- 所有
sendq
和recvq
中的協程的狀態從_Gwaiting
置為_Grunnable
,同樣特權式地將協程插入到調度器的隊列。recvq
中的協程的接收會收到零值,sendq
中的協程的發送會 panic
- select 塊中沒有任何 case 或 default,則協程永遠阻塞
- select 塊中只有一個 case 且沒有 default,則退化為沒有 select 的單個 channel 操作
- select 塊中只有一個 case 和一個 default:
- 退化為 if case else default 的執行
- case 的 channel 操作執行
selectnbsend
/selectnbrecv
函數:- 與
chansend1
/chanrecv1
的差別僅在于:- 如果 channel 為
nil
則返回 - 在最后的阻塞操作之前返回
- 如果 channel 為
- 當且僅當以上情況返回
false
作為 if 的條件
- 與
- select 塊中為其他情況時,通過
selectgo
函數確定一個執行分支:- 隨機確定 case 的遍歷考察順序
- 如果 channel 為
nil
,下一個 - 如果是寫操作:
- 如果 channel 已關閉,則 panic
- 如果有因為讀而阻塞于此的協程,
send
函數 - 否則如果 channel 有緩沖區且未滿,則將消息拷貝到緩沖區尾部
- 以上情況會確定執行此 case,否則下一個
- 如果是讀操作:
- 如果有因為寫而阻塞于此的協程,
recv
函數 - 否則如果 channel 有緩沖區且不空,則將頭部出隊并拷貝到接收變量
- 否則如果 channel 已關閉,則將接收變量置零
- 以上情況會確定執行此 case,否則下一個
- 如果有因為寫而阻塞于此的協程,
- 如果 channel 為
- 如果沒有選擇一個 case 作為執行分支,則執行 default
- 如果沒有 default:
- 加入到所有 channel 的
sendq
或recvq
- 調用
gopark
暫停當前協程 - 等接收或發送操作到來時,此協程會被重新調度,并離開所有加入的
sendq
或recvq
- 加入到所有 channel 的
- 隨機確定 case 的遍歷考察順序
gc
- 非分代,非緊湊,三色標記,寫屏障
- 三色標記:
- 黑:已標記,子對象已考察,不在隊列中
- 灰:已標記,子對象待考察,在隊列中
- 白:未標記
- 觸發:
- 堆:使用量達到動態計算的閾值,
mallocgc
函數 - 時間:上次 gc 后達到兩分鐘,
forcegchelper
函數 - 主動:
GC
函數
- 堆:使用量達到動態計算的閾值,
- 總體過程:
- 進入標記階段,
gcStart
函數:- 獲取
worldsema
信號量 - 確保每個線程當中都有一個執行標記任務的協程
gcBgMarkWorker
- STW!
stopTheWorldWithSema
函數 -
gcphase
從_GCoff
置為_GCmark
,啟動寫屏障 - 統計 root 區塊數量,
gcMarkRootPrepare
函數 - start the world,
startTheWorldWithSema
函數
- 獲取
- 標記:
- 觸發:如前文所描述,
schedule
函數選擇運行一個標記任務協程 - 處理所有灰色對象,
gcDrain
函數:- 尋址到一個 root,標記 root,
markroot
函數:- 將對象標灰加入隊列,
greyobject
函數
- 將對象標灰加入隊列,
- 選擇一個灰色對象移出隊列,將該對象所引用的對象標灰加入隊列,
scanobject
函數
- 尋址到一個 root,標記 root,
- 觸發:如前文所描述,
- 標記完成,進入清掃階段,
gcMarkDone
函數:- 觸發:在
gcBgMarkWorker
中調用 - STW!
-
gcphase
置為_GCmarktermination
- 處理寫屏障的記錄,
wbBufFlush1
函數:將寫屏障記錄的,在標記階段發生變化而遺漏的對象標記,必須在 STW 之下進行,否則此時可能又有變化,將無限循環 - 喚醒清掃任務協程,
gcSweep
函數:調用ready
函數將sweep.g
的狀態置為_Grunnable
加入調度器隊列 -
gcphase
置為_GCoff
,關閉寫屏障 - start the world
- 釋放
worldsema
信號量
- 觸發:在
- 清掃:
- 觸發:如前文所描述,
gcSweep
喚醒清掃任務協程 - 釋放一個申請的堆空間,
sweepone
函數
- 觸發:如前文所描述,
- 進入標記階段,
tcp (on Linux)
-
net.ListenTCP
函數:-
net.ListenTCP
->net.sysListener.listenTCP
->net.internetSocket
->net.socket
->net.netFD.listenStream
- 在
net.socket
->net.sysSocket
中調用系統調用socket
新建監聽套接字的文件描述符 - 在
net.netFD.listenStream
方法中監聽端口:- 調用系統調用
bind
和listen
- 在
internal/poll.pollDesc.init
方法中處理 epoll:- 在
poll_runtime_pollServerInit
函數中調用 Linux APIepoll_create1
創建 epoll 的文件描述符,賦予全局變量epfd
,整個進程中只調用一次 - 在
poll_runtime_pollOpen
函數中調用 Linux APIepoll_ctl(EPOLL_CTL_ADD)
將監聽套接字注冊到 epoll
- 在
- 調用系統調用
-
-
net.TCPListener.AcceptTCP
方法:-
net.TCPListener.AcceptTCP
->net.TCPListener.accept
->net.netFD.accept
- 在
net.netFD.accept
方法中獲取一個連接套接字:- 在
internal/poll.FD.Accept
方法中獲取一個連接套接字:- 調用系統調用
accept4
- 如果獲取不到,在
poll_runtime_pollWait
函數中調用gopark
函數暫停協程。如前文所描述,在findrunnable
/startTheWorldWithSema
等調度過程中,可能調用netpoll
函數,在其中調用 Linux APIepoll_wait
獲取 IO ready 的監聽套接字,將其對應的協程恢復執行
- 調用系統調用
- 在
internal/poll.pollDesc.init
方法中處理 epoll:- 在
poll_runtime_pollOpen
函數中調用 Linux APIepoll_ctl(EPOLL_CTL_ADD)
將連接套接字注冊到 epoll
- 在
- 在
-
-
net.conn.Write
/net.conn.Read
方法:- 調用系統調用
write
/read
- 如果阻塞,如前文所描述調用
poll_runtime_pollWait
暫停協程,且在netpoll
函數中恢復執行 IO ready 的連接套接字對應的協程 -
net.netFD
->internal/poll.FD
->internal/poll.pollDesc
->pollDesc
,一個pollDesc
結構中分別包含一個寫與讀阻塞于此的協程的指針,即寫與讀的阻塞是分開的
- 調用系統調用
Licensed under CC BY-SA 4.0