开天辟地 —— Go scheduler 初始化(二)

这篇博客深入探讨了Go程序的初始化过程,特别是调度器的初始化。从调整SP、初始化g0栈、主线程绑定m0、初始化m0以及初始化allp等方面进行详细阐述,通过源码分析展示了Go程序启动时的关键步骤,揭示了Go调度器如何逐步建立和准备运行环境。
摘要由CSDN通过智能技术生成

上一讲我们说完了 GPM 结构体,这一讲,我们来研究 Go sheduler 结构体,以及整个调度器的初始化过程。

Go scheduler 在源码中的结构体为 schedt,保存调度器的状态信息、全局的可运行 G 队列等。源码如下:

// 保存调度器的信息	
type schedt struct {	
    // accessed atomically. keep at top to ensure alignment on 32-bit systems.	
    // 需以原子访问访问。	
    // 保持在 struct 顶部,以使其在 32 位系统上可以对齐	
    goidgen  uint64	
    lastpoll uint64	
    lock mutex	
    // 由空闲的工作线程组成的链表	
    midle        muintptr // idle m's waiting for work	
    // 空闲的工作线程数量	
    nmidle       int32    // number of idle m's waiting for work	
    // 空闲的且被 lock 的 m 计数	
    nmidlelocked int32    // number of locked m's waiting for work	
    // 已经创建的工作线程数量	
    mcount       int32    // number of m's that have been created	
    // 表示最多所能创建的工作线程数量	
    maxmcount    int32    // maximum number of m's allowed (or die)	
    // goroutine 的数量,自动更新	
    ngsys uint32 // number of system goroutines; updated atomically	
    // 由空闲的 p 结构体对象组成的链表	
    pidle      puintptr // idle p's	
    // 空闲的 p 结构体对象的数量	
    npidle     uint32	
    nmspinning uint32 // See "Worker thread parking/unparking" comment in proc.go.	
    // Global runnable queue.	
    // 全局可运行的 G队列	
    runqhead guintptr // 队列头	
    runqtail guintptr // 队列尾	
    runqsize int32 // 元素数量	
    // Global cache of dead G's.	
    // dead G 的全局缓存	
    // 已退出的 goroutine 对象,缓存下来	
    // 避免每次创建 goroutine 时都重新分配内存	
    gflock       mutex	
    gfreeStack   *g	
    gfreeNoStack *g	
    // 空闲 g 的数量	
    ngfree       int32	
    // Central cache of sudog structs.	
    // sudog 结构的集中缓存	
    sudoglock  mutex	
    sudogcache *sudog	
    // Central pool of available defer structs of different sizes.	
    // 不同大小的可用的 defer struct 的集中缓存池	
    deferlock mutex	
    deferpool [5]*_defer	
    gcwaiting  uint32 // gc is waiting to run	
    stopwait   int32	
    stopnote   note	
    sysmonwait uint32	
    sysmonnote note	
    // safepointFn should be called on each P at the next GC	
    // safepoint if p.runSafePointFn is set.	
    safePointFn   func(*p)	
    safePointWait int32	
    safePointNote note	
    profilehz int32 // cpu profiling rate	
    // 上次修改 gomaxprocs 的纳秒时间	
    procresizetime int64 // nanotime() of last change to gomaxprocs	
    totaltime      int64 // ∫gomaxprocs dt up to procresizetime	
}

在程序运行过程中, schedt 对象只有一份实体,它维护了调度器的所有信息。

在 proc.go 和 runtime2.go 文件中,有一些很重要全局的变量,我们先列出来:

// 所有 g 的长度	
allglen     uintptr	
// 保存所有的 g	
allgs    []*g	
// 保存所有的 m	
allm        *m	
// 保存所有的 p,_MaxGomaxprocs = 1024	
allp        [_MaxGomaxprocs + 1]*p	
// p 的最大值,默认等于 ncpu	
gomaxprocs  int32	
// 程序启动时,会调用 osinit 函数获得此值	
ncpu        int32	
// 调度器结构体对象,记录了调度器的工作状态	
sched       schedt	
// 代表进程的主线程	
m0           m	
// m0 的 g0,即 m0.g0 = &g0	
g0           g

在程序初始化时,这些全局变量都会被初始化为零值:指针被初始化为 nil 指针,切片被初始化为 nil 切片,int 被初始化为 0,结构体的所有成员变量按其类型被初始化为对应的零值。

因此程序刚启动时 allgs,allm 和allp 都不包含任何 g,m 和 p。

不仅是 Go 程序,系统加载可执行文件大概都会经过这几个阶段:

  1. 从磁盘上读取可执行文件,加载到内存

  2. 创建进程和主线程

  3. 为主线程分配栈空间

  4. 把由用户在命令行输入的参数拷贝到主线程的栈

  5. 把主线程放入操作系统的运行队列等待被调度

上面这段描述,来自公众号“ go语言核心编程技术”的调度系列教程。

我们从一个 HelloWorld 的例子来回顾一下 Go 程序初始化的过程:

package main	
import "fmt"	
func main() {	
    fmt.Println("hello world")	
}

在项目根目录下执行:

go build -gcflags "-N -l" -o hello src/main.go

-gcflags"-N -l" 是为了关闭编译器优化和函数内联,防止后面在设置断点的时候找不到相对应的代码位置。

得到了可执行文件 hello,执行:

[qcrao@qcrao hello-world]$ gdb hello

进入 gdb 调试模式,执行 info files,得到可执行文件的文件头,列出了各种段:

640?wx_fmt=png

同时,我们也得到了入口地址:0x450e20。

(gdb) b *0x450e20	
Breakpoint 1 at 0x450e20: file /usr/local/go/src/runtime/rt0_linux_amd64.s, line 8.

这就是 Go 程序的入口地址,我是在 linux 上运行的,所以入口文件为 src/runtime/rt0_linux_amd64.s,runtime 目录下有各种不同名称的程序入口文件,支持各种操作系统和架构,代码为:

TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8	
    LEAQ    8(SP), SI // argv	
    MOVQ    0(SP), DI // argc	
    MOVQ    $main(SB), AX	
    JMP AX

主要是把 argc,argv 从内存拉到了寄存器。这里 LEAQ 是计算内存地址,然后把内存地址本身放进寄存器里,也就是把 argv 的地址放到了 SI 寄存器中。最后跳转到:

TEXT main(SB),NOSPLIT,$-8	
    MOVQ    $runtime·rt0_go(SB), AX	
    JMP AX

继续跳转到 runtime·rt0_go(SB),完成 go 启动时所有的初始化工作。位于 /usr/local/go/src

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值