返回介绍

2. 进程启动时都做了什么?

发布于 2024-04-25 12:55:11 字数 3359 浏览 0 评论 0 收藏 0

下面先看一段程序启动的代码

// runtime/asm_amd64.s

TEXT runtime·rt0_go(SB),NOSPLIT,$0
......此处省略 N 多代码......
ok:
        // set the per-goroutine and per-mach "registers"
        get_tls(BX)  // 将 g0 放到 tls(thread local storage) 里
        LEAQ    runtime·g0(SB), CX
        MOVQ    CX, g(BX)
        LEAQ    runtime·m0(SB), AX

        // save m->g0 = g0  // 将全局 M0 与全局 G0 绑定
        MOVQ    CX, m_g0(AX)
        // save m0 to g0->m
        MOVQ    AX, g_m(CX)

        CLD                             // convention is D is always left cleared
        CALL    runtime·check(SB)

        MOVL    16(SP), AX              // copy argc
        MOVL    AX, 0(SP)
        MOVQ    24(SP), AX              // copy argv
        MOVQ    AX, 8(SP)
        CALL    runtime·args(SB) // 解析命令行参数
        CALL    runtime·osinit(SB) // 只初始化了 CPU 核数
        CALL    runtime·schedinit(SB) // 内存分配器、栈、P、GC 回收器等初始化

        // create a new goroutine to start program
        MOVQ    $runtime·mainPC(SB), AX         // 
        PUSHQ   AX
        PUSHQ   $0                      // arg size
        CALL    runtime·newproc(SB) // 创建一个新的 G 来启动 runtime.main
        POPQ    AX
        POPQ    AX

        // start this M
        CALL    runtime·mstart(SB) // 启动 M0,开始等待空闲 G,正式进入调度循环

        MOVL    $0xf1, 0xf1  // crash
        RET

在启动过程里主要做了这三个事情(这里只跟调度相关的):

  • 初始化固定数量的 P
  • 创建一个新的 G 来启动 runtime.main, 也就是 runtime 下的 main 方法
  • 创建全局 M0、全局 G0,启动 M0 进入第一个调度循环

M0 是什么?程序里会启动多个 M,第一个启动的叫 M0。
G0 是什么?G 分三种,第一种是执行用户任务的叫做 G,第二种执行 runtime 下调度工作的叫 G0,每个 M 都绑定一个 G0。第三种则是启动 runtime.main 用到的 G。写程序接触到的基本都是第一种

我们按照顺序看是怎么完成上面三个事情的。

2.1 runtime.osinit(SB) 方法针对系统环境的初始化

这里实质只做了一件事情,就是获取 CPU 的线程数,也就是 Top 命令里看到的 CPU0、CPU1、CPU2…的数量。

// runtime/os_linux.go

func osinit() {
  ncpu = getproccount()
}

2.2 runtime.schedinit(SB) 调度相关的一些初始化

// runtime/proc.go

// 设置最大 M 数量
sched.maxmcount = 10000

// 初始化当前 M,即全局 M0
mcommoninit(_g_.m)

// 查看应该启动的 P 数量,默认为 cpu core 数.
// 如果设置了环境变量 GOMAXPROCS 则以环境变量为准,最大不得超过_MaxGomaxprocs(1024) 个
procs := ncpu
if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
  procs = n
}
if procs > _MaxGomaxprocs {
  procs = _MaxGomaxprocs
}
// 调整 P 数量,此时由于是初始化阶段,所以 P 都是新建的
if procresize(procs) != nil {
  throw("unknown runnable goroutine during bootstrap")
}

这里 sched.maxmcount 设置了 M 最大的数量,而 M 代表的是系统内核线程,因此可以认为一个进程最大只能启动 10000 个系统线程。

procresize 初始化 P 的数量,procs 参数为初始化的数量,而在初始化之前先做数量的判断,默认是 ncpu(与 CPU 核数相等)。也可以通过环境变量 GOMAXPROCS 来控制 P 的数量。_MaxGomaxprocs 控制了最大的 P 数量只能是 1024。

有些人在进程初始化的时候经常用到 runtime.GOMAXPROCS() 方法,其实也是调用的 procresize 方法重新设置了最大 CPU 使用数量。

2.3 runtime·mainPC(SB) 启动监控任务

// runtime/proc.go

// The main goroutine.
func main() {
  ......
  
  // 启动后台监控
  systemstack(func() {
    newm(sysmon, nil)
  })

  ......
}

在 runtime 下会启动一个全程运行的监控任务,该任务用于标记抢占执行过长时间的 G,以及检测 epoll 里面是否有可执行的 G。下面会详细说到。

2.4 最后 runtime·mstart(SB) 启动调度循环

前面都是各种初始化操作,在这里开启了调度器的第一个调度循环。(这里启动的 M 就是 M0)

下面来围绕 G、M、P 三个概念介绍 Goroutine 调度循环的运作流程。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文