golang internals


中文的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

http://research.swtch.com/goabstract 

http://blog.csdn.net/hopingwhite/article/details/5782888


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM