go內部實現

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。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

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.