golang硬核技术(二)go程序从启动到运行到底经历了啥

news2024/12/23 14:09:18

前言

go相对其他语言,对并发的支持更友好。这使得他的设计和其他程序迥然不同。让我们来看看它都是如何初始化程序的,从程序加载到运行到底经历的什么。

我们继续之前的版本1.18.4

汇编入口

首先我们编译一个hello world 程序。

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

编译时带上调试信息

go build -gcflags=all="-N -l" -ldflags=-compressdwarf=false

用gdb启动,找到入口

image.png

可以看到入口点在rt0_darwin_amd64.s,这里解释一下文件名:

  • rt0 : runtime0 表示起始运行时
  • darwin : 操作系统 我这里是mac系统 对应(GOOS)
  • amd64 : 操作系统架构 对应(GOHOSTARCH)

启动文件位于GOROOT/src/runtime目录下,那同理可以看到其他系统的启动文件

image.png

看一下这个启动文件干了嘛

#include "textflag.h" //定义了一些特殊的宏,用于标记全局符号(函数和全局变量)

//在Go汇编语言中,内存是通过SB伪寄存器定位。SB是Static base pointer 的缩写,意为静态内存的开始地址。
//所有的静态全局符号可以通过SB加一个偏移量定位,而我们定义的符号其实就是相对于SB内存开始地址偏移量。
//对于SB伪寄存器,全局变量和全局函数的符号并没有任何区别。

TEXT _rt0_amd64_darwin(SB),NOSPLIT,$-8 
   JMP    _rt0_amd64(SB) //跳转执行程序的入口函数

TEXT _rt0_amd64_darwin_lib(SB),NOSPLIT,$0
   JMP    _rt0_amd64_lib(SB) //跳转lib库的入口函数

再看下_rt0_amd64函数,位于runtime目录下的asm_amd64.s文件中

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

最终跳转到runtime·rt0_go(SB) 这里是整个go代码的起点,看一下这个函数的重点部分

rt0_go函数

TEXT runtime·rt0_go(SB),NOSPLIT|TOPFRAME,$0
    ...
    // 略过前置设置和检查
    
    // 这段有大量的 m0,g0 tls的分配,设置内容,懒得写略过了
    get_tls(BX) 
    // 将g0保存到tls中
    LEAQ   runtime·g0(SB), CX 
    ...
    
    //运行时类型检查,主要是校验编译器的翻译工作是否正确。
    //基本代码均为检查 int8 在 unsafe.Sizeof 方法下是否等于 1 这类动作。
   CALL   runtime·check(SB)
   ...
   //系统参数传递,主要是将系统参数转换传递给程序使用。
   CALL   runtime·args(SB)
   //系统基本参数设置,主要是获取 CPU 核心数和内存物理页大小
   CALL   runtime·osinit(SB)
   //进行各种运行时组件的初始化,包含调度器、内存分配器、堆、栈、GC 等一大堆初始化工作。
   //会进行 p 的初始化,并将 m0 和某一个 p 进行绑定。
   CALL   runtime·schedinit(SB)

   //创建一个新的 goroutine 来启动程序
   //虽然在runtime·rt0_go 中指向的是$runtime·mainPC,但实质指向的是 runtime.main
   MOVQ   $runtime·mainPC(SB), AX       // entry
   
   //创建一个新的 goroutine,且绑定 runtime.main 方法(也就是应用程序中的入口 main 方法)。
   //并将其放入 m0 绑定的p的本地队列中去,以便后续调度。
   CALL   runtime·newproc(SB)

   //启动 M.mstart,调度器开始进行循环调度。
   CALL   runtime·mstart(SB)

   CALL   runtime·abort(SB)  // M.mstart 应该永不返回
   RET
    ...

从这个汇编文件中,开始调用runtime方法。

go在1.5的版本里就实现了自举,所有大部分的功能都是用go实现的,并多处于runtime文件夹中。

runtime

check

check函数位于runtime的runtime1.go中,主要是检查一些标识,有兴趣的可以自己去看一下。

args

args 函数同样runtime的runtime1.go中。

var (
   argc int32   //参数个数
   argv **byte  //入参
)

func args(c int32, v **byte) { //初始全局变量 argc,argv 并调用sysargs
   argc = c
   argv = v
   sysargs(c, v)
}

var executablePath string  

//获取执行程序路径 复制到全局变量executablePath
func sysargs(argc int32, argv **byte) {
    。。。
}

osinit

osinit位于runtime的os_darwin.go中,我这里是mac系统所以是darwin

func osinit() {
   ncpu = getncpu()             //获取cpu核数
   physPageSize = getPageSize() //获取页大小
}

在mac下,这里实际就是调用sysctl获取系统信息。你也可以理解函数对应命令行是这样的:

  • getncpu() -> sysctl -h -a | grep hw.ncpu
  • getPageSize() -> sysctl -h -a | grep hw.pagesize

schedinit

schedinit位于runtime的proc.go文件中。

func schedinit() {
   //lockInit  锁相关的初始化 暂时忽略

   //获取当前的g 之前已经保存在tls中了,getg就是从tls中获取
   //大致的关系是fs -> tls[1] -> g() -> tls[0] -> g0 -> g0.m0 = &m0 -> m0.g0 = &g0
   //从fs段寄存器出发 找到 m0.tls[1] ,地址-8后得到 tls[0] 而 tls[0]正好指向g0获取到
   _g_ := getg()
   if raceenabled { //如果启用了race 则进行raceinit的初始化,默认false
      _g_.racectx, raceprocctx0 = raceinit()
   }

   //默认m(线程)的最大值是10000个,面试经常问
   sched.maxmcount = 10000

   // The world starts stopped.
   worldStopped() //用于lock rank,可忽略

   moduledataverify() //验证链接器符号,可忽略
   //初始栈,就是初始 stackLarge,stackpool 两个全局变量。对这哥俩感兴趣的可以看上篇博文 内存管理
   //注意这里还没有给栈分配内存
   stackinit()
   //内存分配初始化。就是计算内存大小,初始化mheap,mcache0 等操作
   mallocinit()
   //初始化CPU相关的参数
   //读取环境变量GODEBUG,并调用 internal/cpu.Initialize
   cpuinit()      // must run before alginit
   //map使用必须调用,算法相关
   alginit()      // maps, hash, fastrand must not be used before this call
   //随机数相关
   fastrandinit() // must run before mcommoninit
   //初始化m,调用atomicstorep将m0放入全局变量allm
   //并且将allm挂到m的alllink上
   mcommoninit(_g_.m, -1)
   //模块初始化,将所有模块的moduledata的gc标志初始化,并将moduledata放入全局变量modulesSlice中
   modulesinit()   // provides activeModules
   //type这种别名相关的,消除重复映射
   typelinksinit() // uses maps, activeModules
   //接口相关,将每个模块的itab 放入全局变量itabTable.entries中,方便动态派发
   //itab粗糙的理解 = 接口类型+具体实现类型,方便动态类型的查找。
   itabsinit()     // uses activeModules
   //初始化methodValueCallFrameObjs栈对象 
   stkobjinit()    // must run before GC starts

   //将当前线程信号保存到m.sigmask中,一并设置到全局变量initSigmask
   sigsave(&_g_.m.sigmask)
   initSigmask = _g_.m.sigmask
   ...
   
   goargs() //入参全局变量argslice初始化
   goenvs() //环境全局变量envs初始化
   parsedebugvars() //初始化debug包变量,并根据环境变量GODEBUG解析dbgvars的一系列配置
   gcinit()  //gc相关

   lock(&sched.lock)
   //sched.lastpoll 设置调度器初始化轮训时间
   sched.lastpoll = uint64(nanotime())
   //设置当前cpu个数,在 osinit() 函数里已经获取到。如果环境变量GOMAXPROCS设置了CPU个数,直使用设置个数。
   procs := ncpu
   if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
      procs = n
   }
   //调整cpu 数量
   if procresize(procs) != nil {
      throw("unknown runnable goroutine during bootstrap")
   }
   unlock(&sched.lock)

   ...
   // World is effectively started now, as P's can run.
   worldStarted()

}

mainPC main newproc

上面汇编中将runtime·mainPC作为runtime.newproc的参数,即回调用函数参入。

而上面说的rt0_go函数中还有一段,这里将mainPC指向了main

DATA    runtime·mainPC+0(SB)/8,$runtime·main<ABIInternal>(SB)
GLOBL  runtime·mainPC(SB),RODATA,$8

先看newproc 函数,在runtime的proc.go文件中

//创建一个新的g,绑定main函数,并且加入到队列中等待执行
func newproc(fn *funcval) {
   gp := getg()         //获取当前g
   pc := getcallerpc()  //获取程序计数器
   systemstack(func() {
      //创建新的g并绑定fn,也就是main
      newg := newproc1(fn, gp, pc)
      
      _p_ := getg().m.p.ptr()
      //推入p的队列中
      runqput(_p_, newg, true)

      //是否启动M开始执行
      //默认为false,在下面的main函数中设置mainStarted=true,所以第一次到这里是不会执行的。
      if mainStarted {
         wakep()
      }
   })
}
  • 这里相当于将runtime·main推到p的队列中
  • golang中go statement入口就是newproc,即go func(){}实际上newproc(func(){})的调用

main函数,同样在这个文件里

func main() {
   g := getg()
   g.m.g0.racectx = 0

   //设置栈的最大值,按处理器位数,64位对应1G,32位对应250MB
   if goarch.PtrSize == 8 {
      maxstacksize = 1000000000
   } else {
      maxstacksize = 250000000
   }

   maxstackceiling = 2 * maxstacksize

   mainStarted = true //允许上面的newproc函数创建Ms
   ...
   //执行每runtime的init
   doInit(&runtime_inittask) // Must be before defer.
   
   ...

   gcenable() //开启gc

   //下面一大坨都是cgo相关
   main_init_done = make(chan bool)
   if iscgo {
   ...
   }

   doInit(&main_inittask) //执行package main的init
   ...
   
   fn := main_main // 执行package main中主函数
   fn()

   ...

   exit(0)  //退出进程
}

这里着重说一下doInit函数,它会执行每个模块中的init函数,init函数对应结构体如下:

type initTask struct {
   state uintptr //状态标识 0:未执行, 1:执行中, 2:已完成 
   ndeps uintptr //当前模块的其他依赖
   nfns  uintptr //模块里面的几个init函数
}

看这个结构就能猜到,所有的init函数会根据模块的依赖关系形成一个有向无环图,执行的过程就是对这个图进行深度优先遍历,遍历函数doInit如下

func doInit(t *initTask) {
   switch t.state {
   case 2: // 完成退出
      return
   case 1: // 异常panic
      throw("recursive call during initialization - linker skew")
   default: // 遍历执行
      t.state = 1 // 先设置状态到执行中

      //向下递归
      for i := uintptr(0); i < t.ndeps; i++ {
         p := add(unsafe.Pointer(t), (3+i)*goarch.PtrSize)
         t2 := *(**initTask)(p)
         doInit(t2)
      }

      //当前模块没init则设置状态到完成,返回
      if t.nfns == 0 {
         t.state = 2 // initialization done
         return
      }
      ... //执行当前模块的init,完成后设置状态2 返回
      t.state = 2
   }
}

mstart

mstart函数汇编中指向mstart0

TEXT runtime·mstart(SB),NOSPLIT|TOPFRAME,$0
   CALL   runtime·mstart0(SB)
   RET // not reached

mstart0这个函数同样在proc.go文件里

func mstart0() {
   ...
   mstart1() // 启动m
   //退出当前线程
   if mStackIsSystemAllocated() {
      osStack = true
   }
   //执行完所有的 Goroutine 后,清理并退出m,不会执行到这里
   mexit(osStack)
}


func mstart1() {
   ...
   asminit()
   minit() //初始化新的m,在新线程上调用
   ...
   schedule() //开始调度,找到一个`runnable`状态的goroutine并执行
}

尾语

至此,一整个go的启动流程就串起来了。

再声明一下,博主用的mac环境1.18.4go版本,不同的版本会有稍有差异,尤其是go1.12到1.18内容变动还是比较多的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/708807.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

python--连接oracle数据库

python--连接oracle数据库 前言一、安装cx_Oracle二、导入库三、数据库操作实例3.1 连接3.2数据库查询3.3数据库插入3.4 实例从某网站上面爬取彩票号码 四、异常4.1、运行时&#xff0c;出现连接数据库失败:DatabaseError:DPI-1047解决连接失败问题1、查看Oracle的版本号2、进入…

辅助驾驶功能开发-功能规范篇(21)-1-XP行泊一体方案功能规范

XPilot Safety 主动安全系统 一、前向碰撞预防(Forward Collision Prevention) - 产品定义 基于车辆前向视觉模块和前毫米波雷达的感知能力,对车辆前方扇形区域内的车辆和VRU(弱势道路使用者) 有可能与本车的运行路线发生碰撞,进行预警、碰撞伤害降低或碰撞避免的一系列…

QT 简易视频播放器版本1.1

设计Qt界面实现播放、暂停、停止、下一集、上一集、快进、后退、倍速播放、进度调节&#xff0c;音量调节、视频播放列表等功能 先上演示效果&#xff1a; ui界面设计 videoplayer.h #ifndef VIDEOPLAYER_H #define VIDEOPLAYER_H#pragma execution_character_set("utf-…

深入了解Promise机制并使用JS实现一个Promise(一)

前言 关于为什么会有Promise以及Promise的一些用法和基本机制可以参考之前的文章JS中的异步与Promise使用整体来说&#xff0c;Promise可以帮助我们很好的解决异步的问题&#xff0c;号称是异步的终极解决方案。在浏览器中Promise是使用C实现的&#xff0c;今天就使用js来实现…

JSP原理以及基本语法

1、JSP原理 什么是JSP&#xff1f; Java Server Pages&#xff1a;Java服务器页面&#xff0c;和Servlet一样是动态Web技术&#xff01; 和HTML的区别&#xff1f; HTML是静态页面。在JSP页面中可以嵌入Java代码&#xff0c;为用户提供动态的数据。 JSP 和 Servlet 的关系…

枚举一个进程中的所有线程

在 Win32 开发中&#xff0c;如果需要获取程序运行过程中的一些较为底层的信息&#xff0c;你可能需要使用到 Tool Helper 库。但我愿意称之它为 Win32 中的 “害群之马”。何解&#xff1f; Tool Helper 库在 16 位 Windows 时代就已经存在了&#xff0c;这个库主要用来提供一…

今天给我的Ubuntu服务器挂在了一个4T的硬盘却只能识别到2T,原来是因为这!涨知识了

前言 今天买的4T机械硬盘到了&#xff0c;准备给我的服务器加装上&#xff0c;用来作为Nextcloud的存储硬盘。把硬盘安装好后就迫不及待的进行挂载&#xff0c;挂载的操作倒是挺顺利的&#xff0c;但是无论怎么操作Ubuntu系统识别到的大小居然都是2T&#xff0c;最后没办法&am…

chatgpt赋能python:开方在Python中的用法

开方在Python中的用法 开方是数学中常见的一种运算&#xff0c;用于求一个数的平方根。在Python中&#xff0c;开方运算可以通过使用math模块中的sqrt函数来实现。本文将介绍开方运算的概念、Python中的应用以及一些常见问题的解决方法。 开方的概念 开方是指&#xff0c;对…

chatgpt赋能python:在Python中运行程序的方法介绍

在Python中运行程序的方法介绍 Python是一种广泛使用的编程语言&#xff0c;也是人工智能和数据科学领域的首选。在这篇SEO文章中&#xff0c;我们将介绍Python中运行程序的几种方法。 1. 在Python环境中运行程序 Python环境是一个Python解释器及其标准库的集合。为了在Pyth…

Redis7【⑥ Redis复制(replica)】

Redis复制 Redis 复制&#xff08;Replication&#xff09;是 Redis 的一项核心功能&#xff0c;用于将一个 Redis 数据库的所有数据复制到另一个 Redis 实例上。Redis 复制可以提高系统的可用性、可靠性和扩展性&#xff0c;使得在发生故障时可以快速地恢复数据。 Redis 复制…

【TiDB v7.1.0】资源管控调研及评测

作者&#xff1a; angryart 原文来源&#xff1a; https://tidb.net/blog/ad24240a 多租户是什么 有语云&#xff0c;食在广州&#xff0c;玩在杭州&#xff0c;死在柳州&#xff0c;广东人除了天上飞的飞机不吃&#xff0c;地上走的坦克不吃&#xff0c;其它的什么都吃&am…

Mybatis面试题--MyBatis一级缓存,二级缓存

Mybatis的一级、二级缓存用过吗&#xff1f; 一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存&#xff0c;其存储作用域为 Session&#xff0c;当Session进行flush或close之后&#xff0c;该Session中的所有Cache就将清空&#xff0c;默认打开一级缓存 二级缓存 是基于n…

Python将多维列表「拉伸」为一维列表的10种方式

来源&#xff1a;投稿 作者&#xff1a;Fairy 编辑&#xff1a;学姐 在Python编程中&#xff0c;列表是一种常用的数据类型。当我们遇到了一个嵌套列表&#xff0c;如果想将它扁平化为一维列表&#xff0c;就可以使用下面10种方法之一来实现这个需求。 1. 使用两层循环遍历 l…

【记录】实践场景

Apache Doris 在京东搜索实时 OLAP 探索与实践 https://doris.apache.org/zh-CN/blog/JD_OLAP/ 通过对比开源的几款实时OLAP引擎&#xff0c;我们发现doris和clickhouse能够满足我们的需求&#xff0c;但是clickhouse的并发度太低是个潜在的风险&#xff0c;而且clickhouse的数…

已将该虚拟机配置为使用 64 位客户机操作系统。但是,无法执行 64 位操作。

错误提示&#xff1a; 一般只有下面几种方法 百度经验解决方法 http://jingyan.baidu.com/article/90bc8fc859b481f653640cac.html http://jingyan.baidu.com/article/25648fc1bfd4a29190fd0067.html 2.第二种方法 检测问题所在&#xff1a; 下载LeoMoon CPU-V 检查一下CP…

小程序本地生活

2023年7月1号 感觉就是视频要快点看不完 不然哪天接口又失效了 Page({/*** 页面的初始数据*/data: {// 存放轮播图的数据swiperList:[],// 存放九宫格的数据gridList:[]},/*** 生命周期函数--监听页面加载*/onLoad(options) {this.getSwiperList()this.getGridList()},// 获…

【GIS】阿里AI Earth选择内置地图

说明 aie.Map&#xff0c;构造一个地图组件Map对象&#xff0c;用于可视化渲染计算结果。坐标系固定为EPSG:4326。 阿里AI Earth中&#xff0c;坐标系默认为EPSG:4326 效果 import aie aie.Authenticate() aie.Initialize() my_province aie.FeatureCollection(China_Provin…

【Python】Python基础笔记

Python基础笔记 数据的输入和输出 print("数据") # 这是数据的输出 name input() # 这是数据的输入&#xff0c;并将输入的数据赋值给name。而且无论输入的何种类型的数据&#xff0c;最终的结果都是 字符串 类型的数据pint 输出不换行&#xff1a; # print 输出…

结合ace编辑器实现MapboxGL热力图样式在线配置

概述 MapboxGL热力图的配置参数并不多&#xff0c;但是有时候为了或得一个比较好用的热力图配置参数&#xff0c;我们不得不改代码再预览&#xff0c;显得尤为麻烦&#xff0c;为方便配置&#xff0c;实现实时预览&#xff0c;本文使用ace实现了一个热力图样式在线配置页面。 …

MSF之信息收集及漏洞利用

MSF之信息收集及漏洞利用 一、Metasploit简介二、Metasploit安装三、安装postgresql数据库四、KaIi-msfdb-Postgresql报错排查处理五、Metasploit-启动六、Metasploit-目录结构六、Metasploit-模块七、Metasploit-信息收集7.1、db_nmap/nmap7.2、Metasploit auxiliary7.2.1、端…