• go main.main之前的准备


    程序初始化过程

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

    编译,使用gdb调试。给下列函数下断点:

    _rt0_amd64_darwin
    main
    _rt0_amd64
    runtime.check
    runtime.args
    runtime.osinit
    runtime.hashinit
    runtime.schedinit
    runtime.newproc
    runtime.mstart
    main.main
    runtime.exit

    你可能需要根据自己的系统将_rt0_amd64_darwin改成_rt0_amd64_linux或者别的。在gdb中先点r,回车,然后点c,回车,接着一路回车。

    系统初始化

    整个程序启动是从_rt0_amd64_darwin开始的,然后JMP到main,接着到_rt0_amd64。前面只有一点点汇编代码,做的事情就是通过参数argc和argv等,确定栈的位置,得到寄存器。下面将从_rt0_amd64开始分析。

    这里首先会设置好m->g0的栈,将当前的SP设置为stackbase,将SP往下大约64K的地方设置为stackguard。然后会获取处理器信息,放在全局变量runtime·cpuid_ecx和runtime·cpuid_edx中。接着,设置本地线程存储。本地线程存储是依赖于平台实现的,比如说这台机器上是调用操作系统函数thread_fast_set_cthread_self。设置本地线程存储之后还会立即测试一下,写入一个值再读出来看是否正常。

    本地线程存储

    这里解释一下本地线程存储。比如说每个goroutine都有自己的控制信息,这些信息是存放在一个结构体G中。假设我们有一个全局变量g是结构体G的指针,我们希望只有唯一的全局变量g,而不是g0,g1,g2...但是我们又希望不同goroutine去访问这个全局变量g得到的并不是同一个东西,它们得到的是相对自己线程的结构体G,这种情况下就需要本地线程存储。g确实是一个全局变量,却在不同线程有多份不同的副本。每个goroutine去访问g时,都是对应到自己线程的这一份副本。

    设置好本地线程存储之后,就可以为每个goroutine和machine设置寄存器了。这样设置好了之后,每次调用get_tls(r),就会将当前的goroutine的g的地址放到寄存器r中。你可以在源代码中看到一些类似这样的汇编:

     get_tls(CX)
        MOVQ    g(CX), AX //get_tls(CX)之后,g(CX)得到的就是当前的goroutine的g

    不同的goroutine调用get_tls,得到的g是本地的结构体G的,结构体中记录goroutine的相关信息。

    初始化顺序

    接下来的事情就非常直白,可以直接上代码:

        CLD                // convention is D is always left cleared
        CALL    runtime·check(SB) //检测像int8,int16,float等是否是预期的大小,检测cas操作是否正常
        MOVL    16(SP), AX        // copy argc
        MOVL    AX, 0(SP)
        MOVQ    24(SP), AX        // copy argv
        MOVQ    AX, 8(SP)
        CALL    runtime·args(SB)    //将argc,argv设置到static全局变量中了
        CALL    runtime·osinit(SB)    //osinit做的事情就是设置runtime.ncpu,不同平台实现方式不一样
        CALL    runtime·hashinit(SB)    //使用读/dev/urandom的方式从内核获得随机数种子
        CALL    runtime·schedinit(SB)    //内存管理初始化,根据GOMAXPROCS设置使用的procs等等

    proc.c中有一段注释,也说明了bootstrap的顺序:

    // The bootstrap sequence is:
    //
    // call osinit
    // call schedinit
    // make & queue new G
    // call runtime·mstart
    //
    // The new G calls runtime·main.

    先调用osinit,再调用schedinit,创建就绪队列并新建一个G,接着就是mstart。这几个函数都不太复杂。

    调度器初始化

    让我们看一下runtime.schedinit函数。该函数其实是包装了一下其它模块的初始化函数。有调用mallocinit,mcommoninit分别对内存管理模块初始化,对当前的结构体M初始化。

    接着调用runtime.goargs和runtime.goenvs,将程序的main函数参数argc和argv等复制到了os.Args中。

    也是在这个函数中,根据环境变量GOMAXPROCS决定可用物理线程数目的:

    procs = 1;
        p = runtime·getenv("GOMAXPROCS");
        if(p != nil && (n = runtime·atoi(p)) > 0) {
            if(n > MaxGomaxprocs)
                n = MaxGomaxprocs;
            procs = n;
        }

    回到前面的汇编代码继续看:

      // 新建一个G,当它运行时会调用main.main
        PUSHQ    $runtime·main·f(SB)        // entry
        PUSHQ    $0            // arg size
        CALL    runtime·newproc(SB)
        POPQ    AX
        POPQ    AX
    
        // start this M
        CALL    runtime·mstart(SB)

    先将参数进栈,再被调函数指针和参数字节数进栈,接着调用runtime.newproc函数。所以这里其实就是新开个goroutine执行runtime.main。

    runtime.newproc会把runtime.main放到就绪线程队列里面。本线程继续执行runtime.mstart,m意思是machine。runtime.mstart会调用到调度函数schedule

    schedule函数绝不返回,它会根据当前线程队列中线程状态挑选一个来运行。由于当前只有这一个goroutine,它会被调度,然后就到了runtime.main函数中来,runtime.main会调用用户的main函数,即main.main从此进入用户代码。前面已经写过helloworld了,用gdb调试,一步一步的跟踪观察这个过程。

    main.main就是用户的main函数。这里是指Go的runtime在进入用户main函数之前做的一些事情。

    前面已经介绍了从Go程序执行后的第一条指令,到启动runtime.main的主要流程,比如其中要设置好本地线程存储,设置好main函数参数,根据环境变量GOMAXPROCS设置好使用的procs,初始化调度器和内存管理等等。

    接下来将是从runtime.main到main.main之间的一些过程。注意,main.main是在runtime.main函数里面调用的。不过在调用main.main之前,还有一些工作要做。

    sysmon

    在main.main执行之前,Go语言的runtime库会初始化一些后台任务,其中一个任务就是sysmon。

    newm(sysmon, nil);

    newm新建一个结构体M,第一个参数是这个结构体M的入口函数,也就说会在一个新的物理线程中运行sysmon函数。由此可见sysmon是一个地位非常高的后台任务,整个函数体一个死循环的形式,目前主要处理两个事件:对于网络的epoll以及抢占式调度的检测。大致过程如下:

    for(;;) {
        runtime.usleep(delay);
        if(lastpoll != 0 && lastpoll + 10*1000*1000 > now) {
            runtime.netpoll();
        }
        retake(now);    // 根据每个P的状态和运行时间决定是否要进行抢占
    }

    sysmon会根据系统当前的繁忙程度睡一小段时间,然后每隔10ms至少进行一次epoll并唤醒相应的goroutine。同时,它还会检测是否有P长时间处于Psyscall状态或Prunning状态,并进行抢占式调度。

    scavenger

    scavenger是另一个后台任务,但是它的创建跟sysmon有点区别:

    runtime·newproc(&scavenger, nil, 0, 0, runtime·main);

    newproc创建一个goroutine,第一个参数是goroutine运行的函数。scavenger的地位是没有sysmon那么高的——sysmon是由物理线程运行的,而scavenger只是由goroutine运行的。接下来的章节会说明goroutine与物理线程的区别。

    那么,scavenger执行什么工作?它又为什么不像sysmon那样呢?其实scavenger执行的是runtime·MHeap_Scavenger函数。它将一些不再使用的内存归还给操作系统。Go是一门垃圾回收的语言,垃圾回收会在系统运行过程中被触发,内存会被归还到Go的内存管理系统中,Go的内存管理是基于内存池进行重用的,而这个函数会真正地将内存归还给操作系统。

    scavenger显然没有sysmon要求那么高,所以它仅仅是一个普通的goroutine而不是一个线程。

    main.main在这些后台任务运行起来之后执行,不过在它执行之前,还有最后一个:main.init,每个包的init函数会在包使用之前先执行。

    总结:

    设置好本地线程存储,设置好main函数参数,根据环境变量GOMAXPROCS设置好使用的procs,初始化调度器和内存管理 。初始化一些后台任务, sysmon和scavenger。

  • 相关阅读:
    classic problem: select sortjava
    【转】排序算法复习(Java实现)(二): 归并排序,堆排序,桶式排序,基数排序
    【转】排序算法复习(Java实现)(一): 插入,冒泡,选择,Shell,快速排序
    classic problem: 100 horse and 100 dan
    good chocolate
    【转】Java内存模型 http://blog.csdn.net/silentbalanceyh
    http header/ eclipse package path
    design patterns: factory and abstractFactory
    阅读笔记
    提取Dump文件的Callstack,创建windbg的一个扩展应用
  • 原文地址:https://www.cnblogs.com/peteremperor/p/14555406.html
Copyright © 2020-2023  润新知