<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                合規國際互聯網加速 OSASE為企業客戶提供高速穩定SD-WAN國際加速解決方案。 廣告
                # 2.3 Go 程序啟動引導 Go 程序啟動后需要對自身運行時進行初始化,其真正的程序入口由 runtime 包控制。 以 AMD64 架構上的 Linux 和 macOS 為例,分別位于:`src/runtime/rt0_linux_amd64.s`和`src/runtime/rt0_darwin_amd64.s`。 ``` TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8 JMP _rt0_amd64(SB) TEXT _rt0_amd64_darwin(SB),NOSPLIT,$-8 JMP _rt0_amd64(SB) ``` 可見,兩者均跳轉到了`_rt0_amd64`函數。這種做法符合直覺,在程序編譯為機器碼之后, 依賴特定 CPU 架構的指令集,而操作系統的差異則是直接反應在運行時進行不同的系統級操作上, 例如:系統調用。 > `rt0`其實是`runtime0`的縮寫,意為運行時的創生,隨后所有創建的都是`1`為后綴。 ## 2.3.1 入口參數 操作系統通過入口參數的約定與應用程序進行溝通,為了支持從系統給運行時傳遞參數,Go 程序 在進行引導時將對這部分參數進行處理。程序剛剛啟動時,棧指針 SP 的前兩個值分別對應`argc`和`argv`,分別存儲參數的數量和具體的參數的值: ``` TEXT _rt0_amd64(SB),NOSPLIT,$-8 MOVQ 0(SP), DI // argc LEAQ 8(SP), SI // argv JMP runtime·rt0_go(SB) TEXT runtime·rt0_go(SB),NOSPLIT,$0 // 將參數向前復制到一個偶數棧上 MOVQ DI, AX // argc MOVQ SI, BX // argv SUBQ $(4*8+7), SP // 2args 2auto ANDQ $~15, SP MOVQ AX, 16(SP) MOVQ BX, 24(SP) // 初始化 g0 執行棧 MOVQ $runtime·g0(SB), DI // DI = g0 LEAQ (-64*1024+104)(SP), BX MOVQ BX, g_stackguard0(DI) // g0.stackguard0 = SP + (-64*1024+104) MOVQ BX, g_stackguard1(DI) // g0.stackguard1 = SP + (-64*1024+104) MOVQ BX, (g_stack+stack_lo)(DI) // g0.stack.lo = SP + (-64*1024+104) MOVQ SP, (g_stack+stack_hi)(DI) // g0.stack.hi = SP // 確定 CPU 處理器的信息 MOVL $0, AX CPUID // CPUID 會設置 AX 的值 MOVL AX, SI (...) ``` ## 2.3.2 線程本地存儲 TLS 確定完程序入口參數和 CPU 處理器信息之后,一個影響運行時非常重要的操作便是本地線程存儲 (Thread Local Storage, TLS)。 TEXT runtime·rt0_go(SB),NOSPLIT,$0 (...) #ifdef GOOS_darwin JMP ok // 在 Darwin 系統上跳過 TLS 設置 #endif LEAQ runtime·m0+m_tls(SB), DI // DI = m0.tls CALL runtime·settls(SB) // 將 TLS 地址設置到 DI // 使用它進行存儲,確保能正常運行 MOVQ TLS, BX MOVQ $0x123, g(BX) MOVQ runtime·m0+m_tls(SB), AX CMPQ AX, $0x123 // 判斷 TLS 是否設置成功 JEQ 2(PC) // 如果相等則向后跳轉兩條指令 CALL runtime·abort(SB) // 使用 INT 指令執行中斷 ok: // 程序剛剛啟動,此時位于主線程 // 當前棧與資源保存在 g0 // 該線程保存在 m0 MOVQ TLS, BX LEAQ runtime·g0(SB), CX MOVQ CX, g(BX) LEAQ runtime·m0(SB), AX MOVQ CX, m_g0(AX) // m-&gt;g0 = g0 MOVQ AX, g_m(CX) // g0-&gt;m = m0 (...) 而`g0`和`m0`是一組全局變量,在程序運行之初就已經存在。 除了程序參數外,會首先將 m0 與 g0 通過指針互相關聯。 ``` TEXT runtime·settls(SB),NOSPLIT,$32 ADDQ $8, DI // DI = DI + 8, ELF 格式使用 -8(FS) MOVQ DI, SI // SI = DI MOVQ $0x1002, DI // 0x1002 == ARCH_SET_FS MOVQ $SYS_arch_prctl, AX SYSCALL CMPQ AX, $0xfffffffffffff001 // 驗證是否成功 JLS 2(PC) MOVL $0xf1, 0xf1 // 崩潰 RET ``` 可以看到到此函數進行`arch_prctl`系統調用并`ARCH_SET_FS`作為參數傳遞, 為 FS 段寄存器設置了基礎。 ## 2.3.3 早期校驗與系統級初始化 在正式初始化運行時組件之前,還需要做一些校驗和系統級的初始化工作,這包括:運行時類型檢查, 系統參數的獲取以及影響內存管理和程序調度的相關常量的初始化。 ``` TEXT runtime·rt0_go(SB),NOSPLIT,$0 (...) CALL runtime·check(SB) MOVL 16(SP), AX // 復制 argc MOVL AX, 0(SP) MOVQ 24(SP), AX // 復制 argv MOVQ AX, 8(SP) CALL runtime·args(SB) CALL runtime·osinit(SB) (...) ``` ### 運行時類型檢查 首先要進行的便是運行時類型檢查,由`check`來完成。 其本質上基本上屬于對編譯器翻譯工作的一個校驗,顯然如果編譯器的編譯工作 不正確,運行時的運行過程便不是一個有效的過程。這里粗略展示整個函數的內容: ``` // runtime/runtime1.go func check() { var ( a int8 b uint8 (...) ) (...) // 校驗 int8 類型 sizeof 是否為 1,下同 if unsafe.Sizeof(a) != 1 { throw("bad a") } if unsafe.Sizeof(b) != 1 { throw("bad b") } (...) } ``` ### 系統參數、處理器與內存常量 `argc, argv`作為來自操作系統的參數傳遞給`args`處理程序參數的相關事宜。 ``` // runtime/runtime1.go func args(c int32, v **byte) { argc = c argv = v sysargs(c, v) } ``` ![](https://golang.design/under-the-hood/assets/proc-stack.png)**圖 5.1:ELF 格式的進程棧結構** `args`函數將參數指針保存到了`argc`和`argv`這兩個全局變量中, 供其他初始化函數使用,而后調用了平臺特定的`sysargs`。 對于 Darwin 系統而言,只負責獲取程序的`executable_path`: ``` // runtime/os_darwin.go //go:linkname executablePath os.executablePath var executablePath string func sysargs(argc int32, argv **byte) { // 跳過 argv, envv 與第一個字符串為路徑 n := argc + 1 for argv_index(argv, n) != nil { n++ } executablePath = gostringnocopy(argv_index(argv, n+1)) (...) } ``` 這個參數用于設置`os`包中的`executablePath`變量。 而在 Linux 平臺中,這個過程就變得復雜起來了。 與 Darwin 使用`mach-o`不同,Linux 使用 ELF 格式 \[Matz et al. 2014\]。 ELF 除了 argc, argv, envp 之外,會攜帶輔助向量(auxiliary vector) 將某些內核級的信息傳遞給用戶進程,例如**內存物理頁大小**。具體結構如圖 5.1 所示。 對照圖 5.1 的詞表,我們能夠很容易的看明白`sysargs`在 Linux amd64 下作的事情: ``` // runtime/os_linux.go // physPageSize 是操作系統的內存物理頁字節大小。 // 內存頁的映射和反映射操作必須以 physPageSize 的整數倍完成 var physPageSize uintptr func sysargs(argc int32, argv **byte) { // 跳過 argv, envp 來獲取 auxv n := argc + 1 for argv_index(argv, n) != nil { n++ } n++ // 跳過 NULL 分隔符 // 嘗試讀取 auxv auxv := (*[1 &lt;&lt; 28]uintptr)(add(unsafe.Pointer(argv), uintptr(n)*sys.PtrSize)) if sysauxv(auxv[:]) != 0 { return } // 處理無法讀取 auxv 的情況: // 一種方法是嘗試讀取 /proc/self/auxv。 // 如果這個文件不存在,還可以嘗試調用 mmap 等內存分配的系統調用直接測試物理頁的大小。 (...) } func sysauxv(auxv []uintptr) int { var i int // 依次讀取 auxv 鍵值對 for ; auxv[i] != _AT_NULL; i += 2 { tag, val := auxv[i], auxv[i+1] switch tag { case _AT_PAGESZ: // 讀取內存頁的大小 physPageSize = val // 這里其實也可能出現無法讀取到物理頁大小的情況,但后續再內存分配器初始化的時候還會對 // physPageSize 的大小進行檢查,如果讀取失敗則無法運行程序,從而拋出運行時錯誤 (...) } (...) } return i / 2 } ``` 因此對于 Linux 而言,物理頁大小在`sysargs`中便能直接完成初始化。 最后是,`osinit`完成對 CPU 核心數的獲取,因為這與調度器有關。 而 Darwin 上由于使用的是`mach-o`格式,在此前的`sysargs`上 還沒有確定內存頁的大小,因而在這個函數中,還會額外使用`sysctl`完成物理頁大小的查詢。 ``` var ncpu int32 // Linux func osinit() { ncpu = getproccount() } // Darwin func osinit() { ncpu = getncpu() physPageSize = getPageSize() // 內部使用 sysctl 來獲取物理頁大小. } ``` > `Darwin`從操作系統發展來看,是從 NeXTSTEP 和 FreeBSD 2.x 發展而來的后代, macOS 系統調用的特殊之處在于它提供了兩套調用接口,一個是 Mach 調用,另一個則是 POSIX 調用。 Mach 是 NeXTSTEP 遺留下來的產物,其 BSD 層本質上是對 Mach 內核的一層封裝。 盡管用戶態進程可以直接訪問 Mach 調用,但出于通用性的考慮, 物理頁大小獲取的方式是通過 POSIX`sysctl`這個系統調用進行獲取 \[Bacon, 2007\]。 > > 事實上`Linux`與`Darwin`下的系統調用如何參與到 Go 程序中去稍有不同,我們暫時不做深入討論,留到以后再統一分析。 可以看出,對運行時最為重要的兩個系統級參數:CPU 核心數與內存物理頁大小。 ## 2.3.4 運行時組件核心 萬事俱備只欠東風,對于 Go 運行時而言,最后的這三個函數及其后續 調用關系完整實現了整個程序的全部運行時機制的準備工作: ``` TEXT runtime·rt0_go(SB),NOSPLIT,$0 (...) // 調度器初始化 CALL runtime·schedinit(SB) // 創建一個新的 goroutine 來啟動程序 MOVQ $runtime·mainPC(SB), AX PUSHQ AX PUSHQ $0 // 參數大小 CALL runtime·newproc(SB) POPQ AX POPQ AX // 啟動這個 M,mstart 應該永不返回 CALL runtime·mstart(SB) (...) RET ``` 其中: 1. `schedinit`:進行各種運行時組件初始化工作,這包括我們的調度器與內存分配器、回收器的初始化 2. `newproc`:負責根據主 goroutine (即`main`)入口地址創建可被運行時調度的執行單元 3. `mstart`:開始啟動調度器的調度循環 編譯器負責生成了`main`函數的入口地址,`runtime.mainPC`在數據段中被定義為`runtime.main`保存主 goroutine 入口地址: ~~~ DATA runtime·mainPC+0(SB)/8,$runtime·main(SB) GLOBL runtime·mainPC(SB),RODATA,$8 ~~~ 最后我們來大致瀏覽一下`schedinit`的全貌。`schedinit`函數名表面上是調度器的初始化,但實際上它包含了所有核心組件的初始化工作。 ``` // src/runtime/proc.go func schedinit() { _g_ := getg() (...) // 棧、內存分配器、調度器相關初始化 sched.maxmcount = 10000 // 限制最大系統線程數量 stackinit() // 初始化執行棧 mallocinit() // 初始化內存分配器 mcommoninit(_g_.m) // 初始化當前系統線程 (...) gcinit() // 垃圾回收器初始化 (...) // 創建 P // 通過 CPU 核心數和 GOMAXPROCS 環境變量確定 P 的數量 procs := ncpu if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok &amp;&amp; n &gt; 0 { procs = n } procresize(procs) (...) } ``` 我們最感興趣的三大運行時組件在如下函數簽名中進行大量初始化工作: * `stackinit()`goroutine 執行棧初始化 * `mallocinit()`內存分配器初始化 * `mcommoninit()`系統線程的部分初始化工作 * `gcinit()`垃圾回收器初始化 * `procresize()`根據 CPU 核心數,初始化系統線程的本地緩存 ## 2.3.5 小結 我們通過一個簡化的調用關系圖來對本節中我們觀察到的程序啟動流程,如圖 5.2 所示。 ![](https://golang.design/under-the-hood/assets/boot.png)**圖 5.2:Go 程序引導過程調用關系** 根據分析我們可以看到,Go 程序既不是從`main.main`直接啟動,也不是從`runtime.main`直接啟動。 相反,其實際的入口位于`runtime._rt0_amd64_*`。隨后會轉到`runtime.rt0_go`調用。在這個調用中,除了進行運行時類型檢查外,還確定了兩個很重要的運行時常量,即處理器核心數以及內存物理頁大小。 程序引導和初始化工作是整個運行時最關鍵的基礎步驟之一。在`schedinit`這個函數的調用過程中, 還會完成整個程序運行時的初始化,包括調度器、執行棧、內存分配器、調度器、垃圾回收器等組件的初始化。 最后通過`newproc`和`mstart`調用進而開始由調度器轉為執行主 goroutine。 運行時組件的內容我們留到組件各自的章節中進行討論,我們在下一節中著先著重討論當一切都初始化好后, 程序的正式啟動過程,即`runtime.main`。 ## 進一步閱讀的參考文獻 * \[Matz et al. 2014\] Michael Matz, Jan Hubicka, Andreas Jaeger, Mark Mitchell. System V Application Binary Interface: AMD64 Architecture Processor Supplement. Nov, 2017.[https://www.uclibc.org/docs/psABI-x86\_64.pdf](https://www.uclibc.org/docs/psABI-x86_64.pdf) * \[Bacon, 2007\] Jean Bacon. UNIX family tree. Operating System Foundations Lecture Notes, part 4. Last access: Jan, 2020.[https://www.cl.cam.ac.uk/teaching/0708/OSFounds/P04-4.pdf](https://www.cl.cam.ac.uk/teaching/0708/OSFounds/P04-4.pdf)
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看