中文的go語言內部細節的資料幾乎沒有,所以自己研究了一下
聲明:本文內容主要來自本人對源代碼的研究,以及網上找到的一些資料的整理,不保證完全正確性
-------------------------------------------------------
函數調用協議
go語言中使用的是非連續棧。原因是需要支持goroutine。
假設調用 go func(1,2,3) ,func函數會在一個新的go線程中運行,顯然新的goroutine不能和當前go線程用同一個棧,否則會相互覆蓋。
所以對go關鍵字的調用協議與普通函數調用是不同的。不像常規的C語言調用是push參數后直接call func,上面代碼匯編之后會是:
參數進棧
push func
push 12
call runtime.newproc
pop
pop
12是參數占用的大小。在runtime.newproc中,會新建一個棧空間,將棧參數的12個字節拷貝到新棧空間並讓棧指針指向參數。
這時的線程狀態有點像當被調度器剝奪CPU后一樣,pc,sp會被存到類型於類似於進程控制塊的一個結構體struct G內。func被存放在了struct G的entry域,后面進行調度時調度器會讓goroutine從func開始執行。
defer關鍵字調用過程類似於go,不同的是call的是runtime.deferproc
函數返回時,如果其中包含了defer語句,不是調用add xx SP, return
而是call runtime.deferreturn,add 48 sp,return
多值返回還沒研究明白是怎么實現,如果沒記錯,C語言中返回值好像是放在eax的,這個估計要放棧里了。有待考證。
-----------------------------------------------------------------------
編譯過程分析
$GOROOT/src/cmd/gc目錄,這里gc不是垃圾回收的意思,而是go compiler
6g/8g的源文件的主函數是在lex.c
從這個文件可以看到整個編譯的流程。先是利用bison做了詞法分析yyparse()
后面就是語法分析,注釋中有第一步第二步...最后生成目標文件.8或.6,相當於c的.o
go.y是bison的語法定義文件
事實上go在編譯階段也只是將所有的內容按語法分析的結果放入NodeList這個數據結構里,然后export寫成一個*.8(比如i386的架構),這個.8的文件大概是這樣子的:
go object linux 386 go1 X:none
exports automatically generated from
hello.go in package "even"
$$ // exports
package even
import runtime "runtime"
type @"".T struct { @"".id int }
func (@"".this *@"".T "noescape") Id() (? int) { return @"".this.@"".id }
func @"".Even(@"".i int) (? bool) { return @"".i % 2 == 0 }
func @"".odd(@"".i int) (? bool) { return @"".i % 2 == 1 }
$$ // local types
$$
....
可以自己做實驗寫個hello.go,運行go tool 8g hello.go
具體的文件格式,可以參考src/cmd/gc/obj.c里的dumpobj函數的實現
而如果我們在源文件里寫一個import時,它實際上會將這個obj文件導入到當前的詞法分析過程中來,比如
import xxx
它就是會把pkg/amd64-linux/xxx.a加載進來,接着解析這個obj文件
如果我們看go.y的語法分析定義,就會看到許多hidden和there命名的定義,比如import_there, hidden_import等等,這些其實就是從obj文件來的定義。
又比如我們可能會看到一些根本就不存在於源代碼中的語法定義,但是它確實編譯過了,這是因為在編譯過程中源文件被根據需要插入一些其他的碎片進來,比如builtin的一些庫或者自定義的一些lib庫。
理解了這些,基本上就對go的編譯過程有了一個了解,事實上go的編譯過程做的事情也就是把它變成obj完事,至少我們目前沒有看到更多的工作。接下來想要更深入的理解,就要再看xl的實現了,這部分是將obj變成可執行代碼的過程,應該會比較有趣了。
---------------------------------------------------------------------------------------------
runtime中的調度器相關
$GOROOT/src/pkg/runtime目錄很重要,值得好好研究,源代碼可以從runtime.h開始讀起。
goroutine實現的是自己的一套線程系統,語言級的支持,與pthread或系統級的線程無關。
一些重要的結構體定義在runtime.h中。兩個重要的結構體是G和M
結構體G名字應該是goroutine的縮寫,相當於操作系統中的進程控制塊,在這里就是線程的控制結構,是對線程的抽象。
其中包括
goid //線程ID
status//線程狀態,如Gidle,Grunnable,Grunning,Gsyscall,Gwaiting,Gdead等
有個常駐的寄存器extern register G* g被使用,這個是當前線程的線程控制塊指針。amd64中這個寄存器是使用R15,在x86中使用0(GS) 分段寄存器
結構體M名字應該是machine的縮寫。是對機器的抽象,這里是可用的cpu核心。
proc.c中是實現的線程調度相關。
如果有自己寫過操作系統的經驗,看這個會比較過癮
調度器調度的時機是某線程進入系統調用,或申請內存,或由於等待管道而堵塞等
------------------------------------------------------------------------------------------
系統的初始化
proc.c中有一段注釋
// The bootstrap sequence is:
//
// call osinit
// call schedinit
// make & queue new G
// call runtime·mstart
//
// The new G calls runtime·main.
這個可以在$GOROOT/src/pkg/runtime/asm_386.S中看到。go編譯生成的程序應該是從這個文件開始執行的。
// saved argc, argv
...
CALL runtime·args(SB)
CALL runtime·osinit(SB) //這個設置cpu核心數量
CALL runtime·schedinit(SB)
// create a new goroutine to start program
PUSHL $runtime·main(SB) // entry
PUSHL $0 // arg size
CALL runtime·newproc(SB)
POPL AX
POPL AX
// start this M
CALL runtime·mstart(SB)
還記得前面講的go線程的調用協議么?先push參數,再push被調函數和參數字節數,接着調用runtime.newproc
所以這里其實就是新開個線程執行runtime.main
runtime.newproc會把runtime.main放到就緒線程隊列里面。
本線程繼續執行runtime.mstart,m意思是machine。runtime.mstart會調用到schedule
schedule函數絕不返回,它會根據當前線程隊列中線程狀態挑選一個來運行。
然后就調度到了runtime.main函數中來,runtime.main會調用用戶的main函數,即main.main從此進入用戶代碼
總結一下函數調用流程就是
runtime.osinit --> runtime.schedinit --> runtime.newproc --> runtime.mstart --> schedule -->
runtime.main --> main.main
這個可以寫個helloworld了用gdb調試,一步一步的跟
-----------------------------------------------------------------------------------------------
interface的實現
假設我們把類型分為具體類型和接口類型。
具體類型例如type myint int32 或type mytype struct {...}
接口類型是例如type I interface {}
接口類型的值,在內存中的存放形式是兩個域,一個指向真實數據(具體類型的數據)的指針,一個itab指針。
具體見$GOROOT/src/pkg/reflect/value.go 的type nonEmptyInterface struct {...} 定義
itab中包含了數據(具體類型的)的類型描述符信息和一個方法表
方法表就類似於C++中的對象的虛函數表,上面存的全是函數指針。
方法表是在接口值在初始化的時候動態生成的。具體的說:
對每個具體類型,都會生成一個類型描述結構,這個類型描述結構包含了這個類型的方法列表
對接口類型,同樣也生成一個類型描述結構,這個類型描述結構包含了接口的方法列表
接口值被初始化的時候,利用具體類型的方法表來動態生成接口值的方法表。
比如說var i I = mytype的過程就是:
構造一個接口類型I的值,值的第一個域是一個指針,指向mytype數據的一個副本。注意是副本而不是mytype數據本身,因為如果不這樣的話改變了mytype的值,i的值也被改變。
值的第二個域是指向一個動態構造出來的itab,itab的類型描述符域是存mytype的類型描述符,itab的方法表域是將mytype的類型描述符的方法表的對應函數指針拷貝過來。構造itab的代碼在$ROOT/src/pkg/runtime/iface.c中的函數
static Itab* itab(InterfaceType *inter, Type *type, int32 canfail)
這里還有個小細節是類型描述符的方法表是按方法名排序過的,這樣itab的動態構建過程更快一些,復雜度就是O(接口類型方法表長度+具體類型方法表長度)
可能有人有過疑問:編譯器怎么知道某個類型是否實現了某個接口呢?這里正好解決了這個疑問:
在var i I = mytype 的過程中,如果發現mytype的類型描述符中的方法表跟接口I的類型描述符中的方法表對不上,這個初始化過程就會出錯,提示說mytype沒有實現接口中的某某方法。
再暴一個細節,所有的方法,在編譯過程中都被轉換成了函數
比如說 func (s *mytype) Get()會被變成func Get(s *mytype)。
接口值進行方法調用的時候,會找到itab中的方法表的某個函數指針,其第一個參數傳的正是這個接口值的第一個域,即指向具體類型數據的指針。
在具體實現上面還有一些優化過程,比如接口值的真實數據指針那個域,如果真實數據大小是32位,就不用存指針了,直接存數據本身。再有就是對類接口類型interface{},其itab中是不需要方法表的,所以這里不是itab而直接是一個指向真實數據的類型描述結構的指針。
-------------------------------------------------------------------------------------------------
收集的一些關於go internals的鏈接:
http://code.google.com/p/try-catch-finally/wiki/GoInternals
http://research.swtch.com/gopackage
http://research.swtch.com/interfaces