忠于职守 —— sysmon 线程到底做了什么?(九)

本文详细介绍了Go语言sysmon线程的工作原理,包括如何创建和启动sysmon线程,以及sysmon如何进行系统调用抢占和长时间运行goroutine的抢占。sysmon线程通过监控和调整,确保系统的高效运行和公平调度。
摘要由CSDN通过智能技术生成

runtime.main() 函数中,执行 runtime_init() 前,会启动一个 sysmon 的监控线程,执行后台监控任务:

systemstack(func() {	
    // 创建监控线程,该线程独立于调度器,不需要跟 p 关联即可运行	
    newm(sysmon, nil)	
})

sysmon 函数不依赖 P 直接执行,通过 newm 函数创建一个工作线程:

func newm(fn func(), _p_ *p) {	
    // 创建 m 对象	
    mp := allocm(_p_, fn)	
    // 暂存 m	
    mp.nextp.set(_p_)	
    mp.sigmask = initSigmask	
    // ……………………	
    execLock.rlock() // Prevent process clone.	
    // 创建系统线程	
    newosproc(mp, unsafe.Pointer(mp.g0.stack.hi))	
    execLock.runlock()	
}

先调用 allocm 在堆上创建一个 m,接着调用 newosproc 函数启动一个工作线程:

// src/runtime/os_linux.go	
//go:nowritebarrier	
func newosproc(mp *m, stk unsafe.Pointer) {	
    // ……………………	
    ret := clone(cloneFlags, stk, unsafe.Pointer(mp), unsafe.Pointer(mp.g0), unsafe.Pointer(funcPC(mstart)))	
    // ……………………	
}

核心就是调用 clone 函数创建系统线程,新线程从 mstart 函数开始执行。clone 函数由汇编语言实现:

// int32 clone(int32 flags, void *stk, M *mp, G *gp, void (*fn)(void));	
TEXT runtime·clone(SB),NOSPLIT,$0	
    // 准备系统调用的参数	
    MOVL  flags+0(FP), DI	
    MOVQ  stk+8(FP), SI	
    MOVQ  $0, DX	
  MOVQ  $0, R10	

	
  // 将 mp,gp,fn 拷贝到寄存器,对子线程可见	
  MOVQ  mp+16(FP), R8	
  MOVQ  gp+24(FP), R9	
  MOVQ  fn+32(FP), R12	

	
    // 系统调用 clone	
  MOVL  $56, AX	
  SYSCALL	

	
  // In parent, return.	
  CMPQ  AX, $0	
  JEQ  3(PC)	
  // 父线程,返回	
  MOVL  AX, ret+40(FP)	
  RET	

	
  // In child, on new stack.	
  // 在子线程中。设置 CPU 栈顶寄存器指向子线程的栈顶	
  MOVQ  SI, SP	

	
  // If g or m are nil, skip Go-related setup.	
  CMPQ  R8, $0    // m	
    JEQ  nog	
    CMPQ  R9, $0    // g	
  JEQ  nog	

	
  // Initialize m->procid to Linux tid	
  // 通过 gettid 系统调用获取线程 ID(tid)	
  MOVL  $186, AX  // gettid	
  SYSCALL	
  // 设置 m.procid = tid	
  MOVQ  AX, m_procid(R8)	

	
  // Set FS to point at m->tls.	
  // 新线程刚刚创建出来,还未设置线程本地存储,即 m 结构体对象还未与工作线程关联起来,	
    // 下面的指令负责设置新线程的 TLS,把 m 对象和工作线程关联起来	
  LEAQ  m_tls(R8), DI	
  CALL  runtime·settls(SB)	

	
  // In child, set up new stack	
  get_tls(CX)	
  MOVQ  R8, g_m(R9) // g.m = m	
  MOVQ  R9, g(CX) // tls.g = &m.g0	
  CALL  runtime·stackcheck(SB)	

	
nog:	
  // Call fn	
  // 调用 mstart 函数。永不返回	
  CALL  R12	

	
  // It shouldn't return. If it does, exit that thread.	
  MOVL  $111, DI	
    MOVL  $60, AX	
  SYSCALL	
  JMP  -3(PC)  // keep exiting

先是为 clone 系统调用准备参数,参数通过寄存器传递。第一个参数指定内核创建线程时的选项,第二个参数指定新线程应该使用的栈,这两个参数都是通过 newosproc 函数传递进来的。

接着将 m, g0, fn 分别保存到寄存器中,待子线程创建好后再拿出来使用。因为这些参数此时是在父线程的栈上,若不保存到寄存器中,子线程就取不出来了。

这个几个参数保存在父线程的寄存器中,创建子线程时,操作系统内核会把父线程所有的寄存器帮我们复制一份给子线程,所以当子线程开始运行时就能拿到父线程保存在寄存器中的值,从而拿到这几个参数。

之后,调用 clone 系统调用,内核帮我们创建出了一个子线程。相当于原来的一个执行分支现在变成了两个执行分支,于是会有两个返回。这和著名的 fork 系统调用类似,根据返回值来判断现在是处于父线程还是子线程。

如果是父线程,就直接返回了。如果是子线程,接着还要执行一堆操作,例如设置 tls,设置 m.procid 等等。

最后执行 mstart 函数,这是在 newosproc 函数传递进来的。mstart 函数再调用 mstart1,在 mstart1 里会执行这一行:

// 执行启动函数。初始化过程中,fn == nil	
if fn := _g_.m.mstartfn; fn != nil {	
    fn()	
}

之前我们在讲初始化的时候,这里的 fn 是空,会跳过的。但在这里,fn 就是最开始在 runtime.main 里设置的 sysmon 函数,因此这里会执行 sysmon,而它又是一个无限循环,永不返回。

所以,这里不会执行到 mstart1 函数后面的 schedule 函数,也就不会进入 schedule 循环。因此这是一个不用和 p 结合的 m,它直接在后台执行,默默地执行监控任务。

接下来,我们就来看 sysmon 函数到底做了什么?

sysmon 执行一个无限循环,一开始每次循环休眠 20us,之后(1 ms 后)每次休眠时间倍增,最终每一轮都会休眠 10ms。

sysmon 中会进行 netpool(获取 fd 事件)、retake(抢占)、forcegc(按时间强制执行 gc),scavenge heap(释放自由列表中多余的项减少内存占用)等

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值