Go語言 cgo的預(yù)備知識

2018-07-25 16:16 更新

cgo內(nèi)部實現(xiàn)相關(guān)的知識是比較偏底層的,同時與Go系統(tǒng)調(diào)用約定以及的goroutine的調(diào)度都有一定的關(guān)聯(lián),因此這里先寫一些預(yù)備知識。

本節(jié)的內(nèi)容可能需要前面第三章和第五章的一些基礎(chǔ),同時也作為前面沒有提到的一些細節(jié)的繼續(xù)補充。

m的g0棧

Go的運行時庫中使用了幾個重要的結(jié)構(gòu)體,其中M是機器的抽象。每個M可以運行各個goroutine,在結(jié)構(gòu)體M的定義中有一個相對特殊的goroutine叫g(shù)0(還有另一個比較特殊的gsignal,與本節(jié)內(nèi)容無關(guān)暫且不講)。那么這個g0特殊在什么地方呢?

g0的特殊之處在于它是帶有調(diào)度棧的goroutine,下文就將其稱為“m的g0?!?。Go在執(zhí)行調(diào)度相關(guān)代碼時,都是使用的m的g0棧。當(dāng)一個g執(zhí)行的是調(diào)度相關(guān)的代碼時,它并不是直接在自己的棧中執(zhí)行,而是先切換到m的g0棧然后再執(zhí)行代碼。

m的g0棧是一個特殊的棧,g0的分配和普通goroutine的分配過程不同,g0是在m建立時就生成的,并且給它分配的??臻g比較大,可以假定它的大小是足夠大而不必使用分段棧。而普通的goroutine是在runtime.newproc時建立,并且初始??臻g分配得很小(4K),會在需要時增長。不僅如此,m的g0棧同時也是這個m對應(yīng)的物理線程的棧。

這樣就相當(dāng)于擁有了一個“無窮”大小的非分段棧,于是回答了前面提的那個問題:Go使用的是分段棧,初始棧大小很小,當(dāng)發(fā)現(xiàn)棧不夠時會動態(tài)增長。動態(tài)增長是通過進入函數(shù)時插入檢測指令實現(xiàn)的。然而C函數(shù)不使用分段棧技術(shù),并且假設(shè)棧是足夠大的。調(diào)用cgo代碼時,使用的是m的g0棧,這是一個足夠大的不會發(fā)生分段的棧。

函數(shù)newm是新那一個結(jié)構(gòu)體M,其中調(diào)用runtime.allocm分配M的空間。它的g0域是這個分配的:

mp->g0 = runtime·malg(8192);

等等!好像有哪里不對?這個棧并不是真正的“無窮”大的,它只有8K并且不會增長?那么如果調(diào)用的C函數(shù)使用超過8K的棧大小會發(fā)生什么事情呢?讓我們先試一下,我們建立一個文件test.go,內(nèi)容如下:

package main

/*
#include "stdio.h"

void test(int n) {
  char dummy[1024];

  printf("in c test func iterator %d\n", n);
  if(n <= 0) {
    return;
  }
  dummy[n] = '\a';
  test(n-1);
}
#cgo CFLAGS: -g
*/
import "C"

func main() {
    C.test(C.int(20))
}

函數(shù)test被遞歸調(diào)用多次之后,使用的棧空間是超過8K的。然后?程序運行正常,什么也沒發(fā)生。為什么呢?先賣個關(guān)子,到后面再解釋原因。

進入系統(tǒng)調(diào)用

Go的運行時庫對系統(tǒng)調(diào)用作了特殊處理,所有涉及到調(diào)用系統(tǒng)調(diào)用之前,都會先調(diào)用runtime.entersyscall,而在出系統(tǒng)調(diào)用函數(shù)之后,會調(diào)用runtime.exitsyscall。這樣做原因跟調(diào)度器相關(guān),目的是始終維持GOMAXPROCS的數(shù)量,當(dāng)進入到系統(tǒng)調(diào)用時,runtime.entersyscall會將P的M剝離并將它設(shè)置為PSyscall狀態(tài),告知系統(tǒng)此時其它的P有機會運行,以保證始終是GOMAXPROCS個P在運行。

runtime.entersyscall函數(shù)會立刻返回,它僅僅是起到一個通知的作用。那么這跟cgo又有什么關(guān)系呢?這個關(guān)系可大著呢!在執(zhí)行cgo函數(shù)調(diào)用之前,其實系統(tǒng)會先調(diào)用runtime.entersyscall。這是一個很關(guān)鍵的處理,Go把cgo的C函數(shù)調(diào)用像系統(tǒng)調(diào)用一樣獨立出去了,不讓它影響運行時庫。這就回答了前面提出的第二個問題:Go中的goroutine都是協(xié)作式的,運行到調(diào)用runtime庫時就有機會進行調(diào)度。然而C函數(shù)是不會與Go的runtime做這種交互的,所以cgo的函數(shù)不是一個協(xié)作式的,那么如何避免進入C函數(shù)的這個goroutine“失控”?答案就在這里。將C函數(shù)像處理系統(tǒng)調(diào)用一樣隔離開來,這個goroutine也就不必參與調(diào)度了。而其它部分的goroutine正常的運行不受影響。

退出系統(tǒng)調(diào)用

退出系統(tǒng)調(diào)用跟進入系統(tǒng)調(diào)用是一個相反的過程,runtime.exitsyscall函數(shù)會查看當(dāng)前仍然有可用的P,則讓它繼續(xù)運行,否則這個goroutine就要被掛起了。

對于cgo的代碼也是同樣的作用,出了cgo的C函數(shù)調(diào)用之后會調(diào)用runtime.exitsyscall。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號