文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Golang并发编程之调度器初始化的方法是什么

2023-07-05 15:40

关注

本篇内容主要讲解“Golang并发编程之调度器初始化的方法是什么”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Golang并发编程之调度器初始化的方法是什么”吧!

1. 一些全局变量

proc.goruntime.go中有一些很重要的全局的变量,我们将其先列出来:

var (   m0 m // 代表第一个起来的线程,即主线程   g0 g // m0的g0,即 m0.g0 = &g0      allgs   []*g    // 保存所有的g   allglen uintptr // 所有g的长度         allm       *m     // 保存所有的m   gomaxprocs int32  // p的最大个数,默认等于 ncpu   ncpu       int32  // 程序启动时,会调用osinit函数获得此值   sched      schedt // 调度器的结构体对象,全局仅此一份)

程序初始化时,这些全局变量最开始都会被初始化为空值,然后随着一些初始化函数的作用,这些变量才会开始被赋值。

2. main函数之前

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

在项目根目录下执行go build -gcflags "-N -l" -o main main.go-gcflags "-N -l"是为了关闭编译器的优化和函数内联。然后我们使用gdb调试代码:

$ gdb main
GNU gdb (Ubuntu 8.1.1-0ubuntu1) 8.1.1
...
(gdb) info files
Symbols from "/home/chenyiguo/smb_share/go_routine_test/main".
Local exec file:
    `/home/chenyiguo/smb_share/go_routine_test/main', file type elf64-x86-64.
    Entry point: 0x45c220
    0x0000000000401000 - 0x000000000047e357 is .text
    0x000000000047f000 - 0x00000000004b3ecc is .rodata
    0x00000000004b4060 - 0x00000000004b4538 is .typelink
    0x00000000004b4540 - 0x00000000004b4598 is .itablink
    0x00000000004b4598 - 0x00000000004b4598 is .gosymtab
    0x00000000004b45a0 - 0x000000000050ce10 is .gopclntab
    0x000000000050d000 - 0x000000000050d020 is .go.buildinfo
    0x000000000050d020 - 0x000000000051d600 is .noptrdata
    0x000000000051d600 - 0x0000000000524e10 is .data
    0x0000000000524e20 - 0x0000000000553d28 is .bss
    0x0000000000553d40 - 0x00000000005590a0 is .noptrbss
    0x0000000000400f9c - 0x0000000000401000 is .note.go.buildid

可以看到,程序入口地址是0x45c220,继续打断点b *0x45c220进入,可以看到,程序代码的入口就在/usr/local/go/src/runtime/rt0_linux_amd64.s的第8行。

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

进入代码位置,可以看到,其第8行是调到_rt0_amd64(SB)函数运行。

TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
   JMP    _rt0_amd64(SB)

再全局搜索_rt0_amd64,可以发现,在asm_amd64.s中有如下代码,最终会执行到runtime·rt0_go(SB)代码,在asm_amd64.s中,我们可以找到runtime·rt0_go代码的实现,这也是汇编语言。

TEXT _rt0_amd64(SB),NOSPLIT,$-8
   MOVQ   0(SP), DI  // argc
   LEAQ   8(SP), SI  // argv
   JMP    runtime·rt0_go(SB)

rt0_go函数会完成Go程序启动的所有初始化工作,这个函数比较长,也比较复杂,我们可以分段来看:

TEXT runtime·rt0_go(SB),NOSPLIT|TOPFRAME,$0
   // copy arguments forward on an even stack
   MOVQ   DI, AX    // argc
   MOVQ   SI, BX    // argv
   SUBQ   $(5*8), SP    // 3args 2auto
   ANDQ   $~15, SP
   MOVQ   AX, 24(SP)
   MOVQ   BX, 32(SP)

以上一段我们不用深究,第四条指令调整栈(内核主线程栈)顶指针16字节对齐,然后存储了argcargv数组地址。

2.1 初始化g0

注意,此处提及的g0是全局变量g0,即主线程m0m0.g0

// create istack out of the given (operating system) stack.// _cgo_init may update stackguard.MOVQ   $runtime·g0(SB), DI        // g0的地址存放在DI寄存器LEAQ   (-64*1024+104)(SP), BX     // BX=SP-64*1024+104MOVQ   BX, g_stackguard0(DI)      // g0.stackguard0=SP-64*1024+104MOVQ   BX, g_stackguard1(DI)      // g0.stackguard1=SP-64*1024+104MOVQ   BX, (g_stack+stack_lo)(DI) // g0.stack.lo=SP-64*1024+104MOVQ   SP, (g_stack+stack_hi)(DI) // g0.stack.hi=SP

从以上代码可以看出,系统为主线程m0g0在系统线程的栈空间开辟了一个大约有64KB大小的栈,地址范围是SP-64*1024+104 ~ SP。完成以上指令后,系统栈与g0的关系大致如图所示:

Golang并发编程之调度器初始化的方法是什么

2.2 主线程与m0的绑定

LEAQ    runtime·m0+m_tls(SB), DI // DI=&m0.tls
CALL   runtime·settls(SB)        // 调用settls函数设置本地线程存储

// store through it, to make sure it works
get_tls(BX)
MOVQ   $0x123, g(BX)
MOVQ   runtime·m0+m_tls(SB), AX
CMPQ   AX, $0x123
JEQ 2(PC)
CALL   runtime·abort(SB)

前面两段代码,前两条指令通过runtime·settls来设置本地线程存储,后面一段是验证设置是否成功。下面我们看下runtime·settls到底做了什么。

// set tls base to DI
TEXT runtime·settls(SB),NOSPLIT,$32
#ifdef GOOS_android
   // Android stores the TLS offset in runtime·tls_g.
   SUBQ   runtime·tls_g(SB), DI                // 不会走到这里,这是Android系统的
#else
   ADDQ   $8, DI // ELF wants to use -8(FS)    // 这之后,DI存放的就是m0.tls[1]的地址了
#endif
   MOVQ   DI, SI                               // 将DI值赋给SI,即m0.tls[1]的地址,作为系统调用的第二个参数
   MOVQ   $0x1002, DI    // ARCH_SET_FS        // DI是第一个参数,0x1002表示操作ARCH_SET_FS,这是个int类型的code,表示设置FS段寄存器为SI寄存器的值,即m0.tls[1]
   MOVQ   $SYS_arch_prctl, AX                  // 接下来就是系统调用了
   SYSCALL
   CMPQ   AX, $0xfffffffffffff001
   JLS    2(PC)
   MOVL   $0xf1, 0xf1  // crash
   RET

上面代码表明,通过arch_prctl的系统调用,将FS段寄存器的值设置为了m0.tls[1]的地址。操作系统在把线程调离CPU运行时会帮我们把所有寄存器中的值保存在内存中,调度线程起来运行时又会从内存中把这些寄存器的值恢复到CPU,这样,在此之后,工作线程代码就可以通过FS寄存器来找到m.tls。从而,就实现了主线程与m0之间的绑定。

为了读懂以上代码,我们需要知道的是,get_tlsg是宏实现,在runtime/go_tls.h中,如下。所以我们知道,get_tls(r)会将m0.tls的地址赋给r;而看了后面的操作,你就会明白,g(r)则会取出对应的g地址。

#ifdef GOARCH_amd64#define    get_tls(r) MOVQ TLS, r#define    g(r)   0(r)(TLS*1)#endif

2.3 m0和g0的绑定

ok:
   // set the per-goroutine and per-mach "registers"
   get_tls(BX)
   LEAQ   runtime·g0(SB), CX // CX=&g0
   MOVQ   CX, g(BX)          // m0.tls[0]=&g0
   LEAQ   runtime·m0(SB), AX // AX=&m0

   // save m->g0 = g0
   MOVQ   CX, m_g0(AX)       // m0.g0=&g0
   // save m0 to g0->m       
   MOVQ   AX, g_m(CX)        // g0.m = m0

就这样,将g0m0进行了深刻地绑定

Golang并发编程之调度器初始化的方法是什么

2.4 调度器的初始化

在接下来的代码中又是一些需求项的检查,我们直接忽略,看以下代码:

MOVL    24(SP), AX    // copy argc  // AX=argc
MOVL   AX, 0(SP)                    // argc放到栈顶
MOVQ   32(SP), AX    // copy argv   // AX=argv
MOVQ   AX, 8(SP)                    // argv放到SP+8的位置
CALL   runtime·args(SB)             // 处理操作系统传过来的参数和env,无需关心
CALL   runtime·osinit(SB)           // linux系统的osinit没有做很多事,只是赋值了ncpu和物理页大小
CALL   runtime·schedinit(SB)        // 调度器的初始化

调度器的初始化是在runtime.schedinit函数中完成的,是用go代码写的。

// The bootstrap sequence is://// call osinit// call schedinit// make & queue new G// call runtime·mstart//// The new G calls runtime·main.func schedinit() {   // a lot of lock init   ...   // raceinit must be the first call to race detector.   // In particular, it must be done before mallocinit below calls racemapshadow.   // getg函数源码没有定义,在编译的时候由编译器插入,类似下面的代码   // get_tls(CX)   // MOVQ g(CX), BX   _g_ := getg()  // 获取的 _g_ = &g0   if raceenabled {      _g_.racectx, raceprocctx0 = raceinit()   }   sched.maxmcount = 10000  // 操作系统线程个数最多为10000   // a lot of init   ...      // 初始化m0   mcommoninit(_g_.m, -1)      // 一些其他设置,暂时忽略   ...      sched.lastpoll = uint64(nanotime())   // p的数目确定   procs := ncpu   if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {      procs = n   }   // 初始化p   if procresize(procs) != nil {      throw("unknown runnable goroutine during bootstrap")   }      ...}

从上面我们可以看出,虽然在汇编代码中将m0g0进行了一些数据的绑定,但是并没有真正初始化m0。所以在schedinit函数中,我们有两个重要的工作要做:

2.4.1 初始化m0

// Pre-allocated ID may be passed as 'id', or omitted by passing -1.func mcommoninit(mp *m, id int64) {   _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 id >= 0 {      mp.id = id   } else {      mp.id = mReserveID() // 初次从mReserveID()获取到的id=0   }   // random初始化,用于窃取 G   lo := uint32(int64Hash(uint64(mp.id), fastrandseed))   hi := uint32(int64Hash(uint64(cputicks()), ^fastrandseed))   if lo|hi == 0 {      hi = 1   }   // Same behavior as for 1.17.   // TODO: Simplify ths.   if goarch.BigEndian {      mp.fastrand = uint64(lo)<<32 | uint64(hi)   } else {      mp.fastrand = uint64(hi)<<32 | uint64(lo)   }   // 创建用于信号处理的gsignal,只是简单的从堆上分配一个g结构体对象,然后把栈设置好就返回了   mpreinit(mp)   if mp.gsignal != nil {      mp.gsignal.stackguard1 = mp.gsignal.stack.lo + _StackGuard   }   // 把m0挂入全局链表allm中   // Add to allm so garbage collector doesn't free g->m   // when it is just in a register or thread-local storage.   mp.alllink = allm   // NumCgoCall() iterates over allm w/o schedlock,   // so we need to publish it safely.   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)   }}

从函数可以看出,这里并未对传入的m做有关调度的初始化,可以简单认为这个函数只是把m0放到了全局链表allm中后就返回了。

2.4.2 初始化allp

func procresize(nprocs int32) *p {   ...   old := gomaxprocs // 系统初始化的时候, gomaxprocs=0   if old < 0 || nprocs <= 0 {      throw("procresize: invalid arg")   }      ...   // 看看是否需要扩大allp,初始化时len(allp)=0,所以肯定会增长   // Grow allp if necessary.   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 := make([]*p, nprocs)         // Copy everything up to allp's cap so we         // never lose old allocated Ps.         copy(nallp, allp[:cap(allp)])         allp = nallp      }      if maskWords <= int32(cap(idlepMask)) {         idlepMask = idlepMask[:maskWords]         timerpMask = timerpMask[:maskWords]      } else {         nidlepMask := make([]uint32, maskWords)         // No need to copy beyond len, old Ps are irrelevant.         copy(nidlepMask, idlepMask)         idlepMask = nidlepMask         ntimerpMask := make([]uint32, maskWords)         copy(ntimerpMask, timerpMask)         timerpMask = ntimerpMask      }      unlock(&allpLock)   }   // 初始化这些P   // initialize new P's   for i := old; i < nprocs; i++ {      pp := allp[i]      if pp == nil {         pp = new(p)      }      pp.init(i)      atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp))   }   _g_ := getg() // _g_ = g0   if _g_.m.p != 0 && _g_.m.p.ptr().id < nprocs { // 初始化时m0.p=0,所以不会进这个分支      // continue to use the current P      _g_.m.p.ptr().status = _Prunning      _g_.m.p.ptr().mcache.prepareForSweep()   } else {      // release the current P and acquire allp[0].      //      // We must do this before destroying our current P      // because p.destroy itself has write barriers, so we      // need to do that from a valid P.      if _g_.m.p != 0 {         if trace.enabled {            // Pretend that we were descheduled            // and then scheduled again to keep            // the trace sane.            traceGoSched()            traceProcStop(_g_.m.p.ptr())         }         _g_.m.p.ptr().m = 0      }      _g_.m.p = 0      p := allp[0]      p.m = 0      p.status = _Pidle      acquirep(p) // 把p和m0关联起来               if trace.enabled {         traceGoStart()      }   }   // g.m.p is now set, so we no longer need mcache0 for bootstrapping.   mcache0 = nil   // release resources from unused P's   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]      idlepMask = idlepMask[:maskWords]      timerpMask = timerpMask[:maskWords]      unlock(&allpLock)   }   // 将所有的空闲的p放入空闲链表   var runnablePs *p   for i := nprocs - 1; i >= 0; i-- {      p := allp[i]      if _g_.m.p.ptr() == p {         continue      }      p.status = _Pidle      if runqempty(p) {         pidleput(p)      } else {         p.m.set(mget())         p.link.set(runnablePs)         runnablePs = p      }   }   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}

其实,以上代码可以总结如下:

至此,整个调度器中各组件之间的关系如下图所示:

Golang并发编程之调度器初始化的方法是什么

到此,相信大家对“Golang并发编程之调度器初始化的方法是什么”有了更深的了解,不妨来实际操作一番吧!这里是编程网网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     801人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     348人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     311人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     432人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     220人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯