zoukankan      html  css  js  c++  java
  • Golang源码学习:调度逻辑(一)初始化

    本文所使用的Golang为1.14,dlv为1.4.0。

    源代码

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

    开始调试

    root@xiamin:~/study# dlv debug test.go
    Type 'help' for list of commands.
    (dlv) l
    > _rt0_amd64_linux() /root/go/src/runtime/rt0_linux_amd64.s:8 (PC: 0x465800)
    Warning: debugging optimized function
         3:	// license that can be found in the LICENSE file.
         4:
         5:	#include "textflag.h"
         6:
         7:	TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
    =>   8:		JMP	_rt0_amd64(SB)
         9:
        10:	TEXT _rt0_amd64_linux_lib(SB),NOSPLIT,$0
        11:		JMP	_rt0_amd64_lib(SB)
    

    可以看到最开始是从_rt0_amd64_linux执行,然后直接跳转到_rt0_amd64。执行si进入_rt0_amd64。

    (dlv) si
    > _rt0_amd64() /root/go/src/runtime/asm_amd64.s:15 (PC: 0x461c20)
    Warning: debugging optimized function
        10:	// _rt0_amd64 is common startup code for most amd64 systems when using
        11:	// internal linking. This is the entry point for the program from the
        12:	// kernel for an ordinary -buildmode=exe program. The stack holds the
        13:	// number of arguments and the C-style argv.
        14:	TEXT _rt0_amd64(SB),NOSPLIT,$-8
    =>  15:		MOVQ	0(SP), DI	// argc,将参数个数存入DI
        16:		LEAQ	8(SP), SI	// argv,参数数组的地址存入SI
        17:		JMP	runtime·rt0_go(SB)
    

    继续执行,runtime.rt0_go() /root/go/src/runtime/asm_amd64.s:89 (PC: 0x461c30)

    runtime.rt0_go

    runtime.rt0_go中代码较多,但我们只关注与调度相关的。

    TEXT runtime·rt0_go(SB),NOSPLIT,$0
            // 忽略处理命令行参数相关
    
            // 为全局变量g0设置一些栈相关的属性
            MOVQ	$runtime·g0(SB), DI		// 将全局变量g0的存入DI
    	LEAQ	(-64*1024+104)(SP), BX		// bx = SP-(64*1024+104),g0的栈帧大小
    	MOVQ	BX, g_stackguard0(DI)		// g0.stackguard0 = bx
    	MOVQ	BX, g_stackguard1(DI)		// g0.stackguard1 = bx
    	MOVQ	BX, (g_stack+stack_lo)(DI)	// g0.stack.lo = bx    栈的低地址
    	MOVQ	SP, (g_stack+stack_hi)(DI)	// g0.stack.hi = sp    栈的高地址
    
            // 忽略获取cpu型号等相关与cgo初始化
    
            // 线程本地存储(tls)相关设置
            LEAQ	runtime·m0+m_tls(SB), DI	// di = &m0.tls
    	CALL	runtime·settls(SB)		// 设置tls,下面有详细分析
    
            // 验证tls是否生效:通过tls设置一个数值,然后m0.tls[0]获取,与设置的值对比。
    	get_tls(BX)				// 获取fs地址到bx
    	MOVQ	$0x123, g(BX)			// 反编译后 mov qword ptr fs:[0xfffffff8], 0x123,表示设置fs-8地址中的内容为0x123,其实就是m0.tls[0]的地址。
    	MOVQ	runtime·m0+m_tls(SB), AX	// ax = m0.tls[0]
    	CMPQ	AX, $0x123			// 比较
    	JEQ 2(PC)
    	CALL	runtime·abort(SB)
    
    	// m0.tls[0] = &g0;  g0与m0相互绑定
    	get_tls(BX)			// 获取fs地址到bx
    	LEAQ	runtime·g0(SB), CX	// cx = &g0
    	MOVQ	CX, g(BX)		// m0.tls[0] = &g0
    	LEAQ	runtime·m0(SB), AX	// ax = &m0
    	MOVQ	CX, m_g0(AX)		// m0.g0 = &g0
    	MOVQ	AX, g_m(CX)		// g0.m = &m0
    
            // 忽略copy argc和argv的代码
    	CALL	runtime·args(SB)	// 命令行参数相关,暂不关心
    	CALL	runtime·osinit(SB)	// 设置全局变量ncpu(cpu个数),全局变量physHugePageSize
    	CALL	runtime·schedinit(SB)	// 调度器初始化
    
            // 调用runtime·newproc创建goroutine,指向函数为runtime·main
    	MOVQ	$runtime·mainPC(SB), AX	// runtime·mainPC就是runtime·main
    	PUSHQ	AX			// newproc的第二个参数,也就是goroutine要执行的函数。
    	PUSHQ	$0			// newproc的第一个参数,表示要传入runtime·main中参数的大小,此处为0。
    	// 创建 main goroutine。非main goroutine也是此方法创建。
    	// go编译会将语句 go foo() 编译为 runtime·newproc(SB) 并传入参数。
    	CALL	runtime·newproc(SB)	
    	POPQ	AX
    	POPQ	AX
    
    	CALL	runtime·mstart(SB)	// 进入调度循环
    	CALL	runtime·abort(SB)	// mstart应该永不返回,如果返回,则是程序出现错误了。
    	RET
    	MOVQ	$runtime·debugCallV1(SB), AX
    	RET
    DATA	runtime·mainPC+0(SB)/8,$runtime·main(SB)
    GLOBL	runtime·mainPC(SB),RODATA,$8
    

    runtime·settls 设置线程本地存储

    runtime·settls中通过调用arch_prctl系统调用设置FS来实现线程本地存储。

    通过syscall指令调用系统调用

    • rax存放系统调用号,调用返回值也会放在rax中
    • 当系统调用参数小于等于6个时,参数则须按顺序放到寄存器 rdi,rsi,rdx,r10,r8,r9中。
    • 如果系统调用的参数数量大于6个,需将参数保存在一块连续的内存中,并将地址存入rbx中。

    新建非m0的m时也会通过runtime·clone调用此函数。

    TEXT runtime·settls(SB),NOSPLIT,$32
            // 此时di = &m.tls[0]
    	ADDQ	$8, DI			// ELF 需要使用 -8(FS),di+=8,执行完此指令后 di = &m.tls[1]
    	MOVQ	DI, SI			// 将地址移动到si中,作为系统调用的第二个参数
    	MOVQ	$0x1002, DI		// ARCH_SET_FS表示设置FS,作为系统调用的第一个参数
    	MOVQ	$SYS_arch_prctl, AX	// rax存储系统调用号
    	SYSCALL
    	CMPQ	AX, $0xfffffffffffff001	// 比较返回结果
    	JLS	2(PC)
    	MOVL	$0xf1, 0xf1  // crash
    	RET
    

    runtime.schedinit 调度初始化

    runtime.schedinit中包含了很多功能的初始化,本文暂且分析与调度相关的

    func schedinit() {
            
    	_g_ := getg()		// 未找到getg()的源代码,通过注释得知getg()返回当前g,此处 _g_为&g0
            ..........
    	sched.maxmcount = 10000	// m的最大数量为10000
            ..........
    	mcommoninit(_g_.m)	// 此处_g_.m即为m0,对m0的一些初始化工作,下面详细分析
    	..........
    	// 获取要初始化的p的数量,默认与cpu个数相同,如果指定了GOMAXPROCS,则为GOMAXPROCS
    	procs := ncpu
    	if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
    		procs = n
    	}
            // 初始化allp并为allp中的元素初始化、赋值等,详见下方
    	if procresize(procs) != nil {
    		throw("unknown runnable goroutine during bootstrap")
    	}
            ..........
    }
    

    schedinit->mcommoninit

    func mcommoninit(mp *m) {
    	_g_ := getg()	// 获取当前g,也就是g0
    
    	// g0 stack won't make sense for user (and is not necessary unwindable).
    	if _g_ != _g_.m.g0 {
    		callers(1, mp.createstack[:])    // 调用栈相关
    	}
    
    	lock(&sched.lock)
    	if sched.mnext+1 < sched.mnext {
    		throw("runtime: thread ID overflow")
    	}
    	mp.id = sched.mnext	// 设置m的id
    	sched.mnext++		// 加1,以后分配给下一个m
    	checkmcount()		// 检查非空闲数量的m是否超过了10000
    
            // rand相关
    	mp.fastrand[0] = uint32(int64Hash(uint64(mp.id), fastrandseed))
    	mp.fastrand[1] = uint32(int64Hash(uint64(cputicks()), ^fastrandseed))
    	if mp.fastrand[0]|mp.fastrand[1] == 0 {
    		mp.fastrand[1] = 1
    	}
    
            // 新建一个32k栈大小的g,赋值给m0.gsignal。并使 m0.gsignal.m = *m0
    	mpreinit(mp)
    	if mp.gsignal != nil {
    		mp.gsignal.stackguard1 = mp.gsignal.stack.lo + _StackGuard
    	}
            
            // 下面两步将mp放入全局变量allm中,allm是个链表
    	mp.alllink = allm
    	atomicstorep(unsafe.Pointer(&allm), unsafe.Pointer(mp))    
    	unlock(&sched.lock)
    
    	// Allocate memory to hold a cgo traceback if the cgo call crashes.
    	if iscgo || GOOS == "solaris" || GOOS == "illumos" || GOOS == "windows" {
    		mp.cgoCallers = new(cgoCallers)
    	}
    }
    

    mcommoninit基本上就是做一些m0的初始化。

    schedinit->procresize

    // 传入参数nprocs为期望的所有p的个数
    func procresize(nprocs int32) *p {
    	old := gomaxprocs // gomaxprocs在本方法的末尾会被更改
    	if old < 0 || nprocs <= 0 {
    		throw("procresize: invalid arg")
    	}
    	if trace.enabled {
    		traceGomaxprocs(nprocs)
    	}
    
    	// 更新统计信息
    	now := nanotime()
    	if sched.procresizetime != 0 {
    		sched.totaltime += int64(old) * (now - sched.procresizetime)
    	}
    	sched.procresizetime = now
    
    	// 初始化allp
    	if nprocs > int32(len(allp)) {
    		// Synchronize with retake, which could be running
    		// concurrently since it doesn't run on a P.
    		lock(&allpLock)
    		if nprocs <= int32(cap(allp)) {
    			allp = allp[:nprocs]
    		} else {
                             // 初始化一个临时变量nallp,与现存的allp合并,然后将nallp赋值给全局变量allp
    			nallp := make([]*p, nprocs)
    			copy(nallp, allp[:cap(allp)])
    			allp = nallp
    		}
    		unlock(&allpLock)
    	}
    
    	// 初始化新添加到allp中的元素
    	for i := old; i < nprocs; i++ {
    		pp := allp[i]
    		if pp == nil {
    			pp = new(p)
    		}
    		pp.init(i) // 会初始化p结构的属性:id,status,mcache等
    		atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp))    // 赋值
    	}
    
    	_g_ := getg()
            // 初始化的时候 _g_.m.p = 0 所以走else
    	if _g_.m.p != 0 && _g_.m.p.ptr().id < nprocs {
    		// continue to use the current P
    		_g_.m.p.ptr().status = _Prunning
    		_g_.m.p.ptr().mcache.prepareForSweep()
    	} else {
    		// 此处省略一些初始化时不会进入的代码
                    
    		_g_.m.p = 0
    		_g_.m.mcache = nil
    		p := allp[0]
    		p.m = 0
    		p.status = _Pidle
    		acquirep(p)	// m.mcache = p.mcache;p和m相互绑定;p.status = _Prunning。下面有分析。
    		if trace.enabled {
    			traceGoStart()
    		}
    	}
    
    	// 释放未使用的p的资源,比如调用runtime.GOMAXPROCS(num),会调用procresize。
            // num小于当前p的数量时,会执行此处
    	for i := nprocs; i < old; i++ {
    		p := allp[i]
    		p.destroy()
    		// can't free P itself because it can be referenced by an M in syscall
    	}
    
    	// Trim allp.
    	if int32(len(allp)) != nprocs {
    		lock(&allpLock)
    		allp = allp[:nprocs]
    		unlock(&allpLock)
    	}
    
            // 将除了当前m绑定p的其余allp中的都以链表形式存入sched.pidle中
    	var runnablePs *p
    	for i := nprocs - 1; i >= 0; i-- {
    		p := allp[i]
    		if _g_.m.p.ptr() == p {	// 是否是当前g.m的p
    			continue
    		}
    		p.status = _Pidle
    		if runqempty(p) {    
    			pidleput(p)	// 将p放入到空闲列表中
    		} else {
    			p.m.set(mget())
    			p.link.set(runnablePs)
    			runnablePs = p
    		}
    	}
            // 这里会更改gomaxprocs的值
    	stealOrder.reset(uint32(nprocs))
    	var int32p *int32 = &gomaxprocs // make compiler check that gomaxprocs is an int32
    	atomic.Store((*uint32)(unsafe.Pointer(int32p)), uint32(nprocs))
    	return runnablePs
    }
    

    总结一下procresize的工作:

    • allp切片中p的数量小于期望p数量时,对allp进行扩容
    • 使用new创建p并调用p.init初始化刚扩容出的,init中为p分配id和mcache
    • 初始化时,调用acquirep使allp[0]与m0相互绑定,并且m.mcache = p.mcache,p.status = _Prunning
    • allp切片中p的数量大于期望p数量时,调用p.destroy释放未使用的p的资源
    • 将除了allp[0]之外的p状态设置为_Pidle并加入到全局空闲列表sched.pidle中
    • 更改gomaxprocs值为nprocs

    acquirep(p)->wirep(_p_) :acquirep中的主要逻辑就是调用了wirep

    func wirep(_p_ *p) {
    	_g_ := getg()
    
    	if _g_.m.p != 0 || _g_.m.mcache != nil {
    		throw("wirep: already in go")
    	}
    	if _p_.m != 0 || _p_.status != _Pidle {
    		id := int64(0)
    		if _p_.m != 0 {
    			id = _p_.m.ptr().id
    		}
    		print("wirep: p->m=", _p_.m, "(", id, ") p->status=", _p_.status, "
    ")
    		throw("wirep: invalid p state")
    	}
    	_g_.m.mcache = _p_.mcache	// p的mcache赋值给m.mcache
    	_g_.m.p.set(_p_)		// 与下面的一行为 p和m相互绑定
    	_p_.m.set(_g_.m)
    	_p_.status = _Prunning		// 更改p的状态
    }
    
  • 相关阅读:
    Open source physics engine
    Free Platformers: Open Source Gamers Guide to Free Games
    安装路由后,显示已连接,却上不了网?
    http://blog.csdn.net/duanbeibei/article/details/5890436
    javascript权威指南 第8章 笔记2 Kevin
    javascript权威指南 第9章 笔记 Kevin
    javascript权威指南 笔记2 Kevin
    .Net 登录窗口 Kevin
    C# 中读XML时haschrildnodes方法老为true Kevin
    javascript权威指南 第8章 笔记 Kevin
  • 原文地址:https://www.cnblogs.com/flhs/p/12657348.html
Copyright © 2011-2022 走看看