Golang 運行時學習筆記

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 個桶的數據會遷移到新第 ii + 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-1b-8 存放主調函數的 BP偽 SP 和 當前 BP 指向 b-8
    • b-9c 存放局部變量 var0 到 varN
    • cd 存放被調函數的返回值 retN 到 ret0 和 參數 argN 到 arg0,偽 FP 和 當前 SP 指向 d
    • d-1d-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 函數:
    • 切換至調度協程 g0mcall 函數:匯編實現,保存當前協程程序指針、棧指針,設置 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 接收,chanrecv1 函數:
    • 如果 channel 為 nil,則協程永遠阻塞
    • 如果 channel 已關閉且緩沖區為空,則將接收變量置零并返回
    • 如果有因為寫而阻塞于此的協程,recv 函數:
      • 將發送方的 sudog 移出 sendq
      • 將消息從在 sudog 中的發送變量的地址拷貝,如果有緩沖區且不空,則將消息拷貝到緩沖區尾部,將頭部出隊并拷貝到接收變量,否則將消息拷貝到接收變量
      • 將發送方協程的狀態從 _Gwaiting 置為 _Grunnable,同樣特權式地將協程插入到調度器的隊列
      • 接收方協程不會阻塞,狀態始終是 _Grunning
    • 否則如果 channel 有緩沖區且不空,則將頭部出隊并拷貝到接收變量
    • 否則,阻塞接收,過程與發送對偶
  • 關閉 channel,closechan 函數:
    • 如果 channel 為 nil 或已關閉,則 panic
    • 所有 sendqrecvq 中的協程的狀態從 _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 則返回
        • 在最后的阻塞操作之前返回
      • 當且僅當以上情況返回 false 作為 if 的條件
  • select 塊中為其他情況時,通過 selectgo 函數確定一個執行分支:
    • 隨機確定 case 的遍歷考察順序
      • 如果 channel 為 nil,下一個
      • 如果是寫操作:
        • 如果 channel 已關閉,則 panic
        • 如果有因為讀而阻塞于此的協程,send 函數
        • 否則如果 channel 有緩沖區且未滿,則將消息拷貝到緩沖區尾部
        • 以上情況會確定執行此 case,否則下一個
      • 如果是讀操作:
        • 如果有因為寫而阻塞于此的協程,recv 函數
        • 否則如果 channel 有緩沖區且不空,則將頭部出隊并拷貝到接收變量
        • 否則如果 channel 已關閉,則將接收變量置零
        • 以上情況會確定執行此 case,否則下一個
    • 如果沒有選擇一個 case 作為執行分支,則執行 default
    • 如果沒有 default:
      • 加入到所有 channel 的 sendqrecvq
      • 調用 gopark 暫停當前協程
      • 等接收或發送操作到來時,此協程會被重新調度,并離開所有加入的 sendqrecvq

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 函數
    • 標記完成,進入清掃階段,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 方法中監聽端口:
      • 調用系統調用 bindlisten
      • internal/poll.pollDesc.init 方法中處理 epoll:
        • poll_runtime_pollServerInit 函數中調用 Linux API epoll_create1 創建 epoll 的文件描述符,賦予全局變量 epfd,整個進程中只調用一次
        • poll_runtime_pollOpen 函數中調用 Linux API epoll_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 API epoll_wait 獲取 IO ready 的監聽套接字,將其對應的協程恢復執行
      • internal/poll.pollDesc.init 方法中處理 epoll:
        • poll_runtime_pollOpen 函數中調用 Linux API epoll_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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內容