[Golang]OS系統呼叫淺析
上回講Goroutine狀態變換的時候,遺留了一部分關於Syscall處理的內容,這次打算把Go語言對Syscall的處理機制系統的總結一下,放在今天這篇文章中。
Go 語言庫對Syscall的封裝
我們知道Go是一門面向系統級開發的Native程式語言,與C/C++ 類似,Go的編譯器會直接將程式編譯、連結成本地可執行檔案。理論上,它可以完成任何C/C++語言能完成的。作為支撐該特性的重要方面,Go以標準庫形式提供了syscall包,用來支援OS級系統呼叫。
首先,Go對各種系統呼叫介面進行了封裝,提供給使用者一組Go語言函式,方便在程式中直接呼叫,如:
|
同時,Go還通過以下函式提供了對Syscall的直接呼叫支援:
|
其中,帶有Raw
字首的一組操作表示直接呼叫syscall (注:以Linux為例,在AMD64中是通過syscall
指令實現,在X86中是int 0x80
軟中斷,而ARM中則是採用SWI
軟中斷實現系統呼叫),而不帶Raw
字首的操作則在真正呼叫syscall前會先呼叫runtime·entersyscall
,並在syscall返回後插入runtime·exitsyscall
。這兩個輔助函式的功能我們在前面介紹排程器時已經說過了,後面還會再提。
這4個函式全都是用匯編語言實現的,並且和具體的硬體架構及OS相關,比如Linux下ARM架構的相應實現,在 src/pkg/syscall/asm_linux_arm.s
中。至於其他的如Read/Write
這類的函式,其內部基本上都呼叫上面的4個函式實現的。
執行時支援
我們之前講了很多次,Go語言runtime為了實現較高的併發度,對OS系統呼叫做了一些優化,主要就體現在runtime·entersyscall
和入runtime·exitsyscall
這兩個函式上,它們的實現程式碼在src/pkg/runtime/proc.c
之中,之前我們已經多次討論過這個檔案了。
在分析實現代前,我們先來看看函式的宣告,位置在src/pkg/runtime/runtime.h
中:
|
這裡聲明瞭3個函式,多了一個void runtime·entersyscallblock(void)
,在後面會分析它的功能和使用情況。
好了,現在來看實現程式碼。首先,我們很容易找到了void runtime·exitsyscall(void)
的實現,而另外兩個卻找不到,只是找到了兩個與之向接近的函式定義,分別是:
|
通過反彙編分析,我發現程式碼中所有對runtime·entersyscall
和runtime·entersyscallblock
的呼叫最後都分別對映到了·entersyscall
和·entersyscallblock
,也就是說前面兩個函式分別是後面兩個函式的別名。至於為什麼這樣實現,我沒有找到相關的文件說明,但感覺應該主要是由於前後兩組函式引數不同的關係 —— 函式呼叫本身是不需要傳入引數的,而函式實現時,無中生有了一個dummy
引數,其目的就是為了通過該引數指標(地址)方便定位呼叫者的PC和SP值。
runtime·entersyscall
好了,我們回到函式實現分析上來,看看進入系統呼叫前,runtime究竟都做了那些特別處理。下面將這個函式分成3段進行分析:
首先,函式通過“pragma”將該函式宣告為“NOSPLIT”,令其中的函式呼叫不觸發棧擴充套件檢查。
剛進入函式,先禁止搶佔,然後通過
dummy
引數獲得呼叫者的SP和PC值(通過save
函式儲存到g->sched.sp
和g->sched.pc
),將其分別儲存到groutine的syscallsp
和syscallpc
欄位,同時記錄的欄位還有syscallstack
和syscallguard
。這些欄位的功能主要是使得垃圾收集器明確棧分析的邊界 —— 對於正在進行系統呼叫的任務,只對其進入系統呼叫前的棧進行“標記-清除”。(實際上,Go語言的cgo機制也利用了entersyscall
,因而cgo執行的程式碼不受垃圾收集機制管理。)然後,Goroutine的狀態切換到
Gsyscall
狀態。
|
- 下面的程式碼是喚醒runtime的後臺監控執行緒
sysmon
,在之前講排程器的時候說過,sysmon
會監控所有執行syscall的執行緒M,一旦超過某個時間閾值,就將該M與對應的P解耦。
|