分类 go语言 下的文章

GMP源码剖析

1. GMP简介

G: goroutine,用户级线程或协程,在用户态调度  
M:machine,内核级线程,在内核态调度  
P:processor,存放G队列,P必须绑定M,队列中的G才可以执行  
m0:主线程对应的结构  
g0:每个M都绑定一个g0,栈的大小为8K,用来执行runtime的代码  

2. 入口函数

_rt0_【ARCH】_【OS】,例如ARCH为amd64,OS为linux的入口函数为_rt0_amd64_linux,如下图:

entry_20211203170821.png

3. TLS

TLS(Thread Local Storage),每个线程会绑定m.tls的地址,tls类型为[6]uintptr,首个元素为当前运行的g,g中保存m

4. 运行前的准备工作

a. 初始化m0所绑定的g0的栈,大小约64k,区别于其他m所绑定的栈为8K的g0,如下图:  

stack_20211203172931.png

b. 绑定m0与g0,如下图:

bind_20211203173621.png

c. 命令行参数、系统信息、调度等初始化,如下图:

init_20211203173928.png

5. 主goroutine的创建

main函数所在的goroutine的创建,如下图:  
其它goroutine由go关键字来创建,编译器会自动把go关键字转成newproc函数

main_20211203174652.png

6. goroutine调度

每个的线程的入口函数都是mstart,mstart执行schedule,schedule依次从本地P、全局P、其它P、netpoll中取g  
如果都没有取到,线程则休眠,一旦取到,则调用execute来执行,若当前goroutine执行完毕,则调用goexit0继续调度
自动执行goexit0的原因在于创建goroutine的时候,把goexit的函数地址设定到goroutine根函数返回时pop所在的栈位置  
ret指令会执行pop pc,如下图:

exit_20211203181103.png

call_20211203181816.png

7. 系统监控

goroutine会专门启动一个线程来处理netpoll、长时间运行的goroutine,如下图:
a. 如果已经有可读或可写的goroutine并且超过10ms得不到执行,则加入全局P中
b. 如果某个goroutine长时间处于运行状态超过10ms,则会被标记成抢占状态,待调用某个函数时因栈溢出触发morestack函数,进而被其他goroutine抢占
c. 如果某个goroutine处于系统调用状态,如果此时本地P还有别的goroutine,或者执行时间超过10ms,则唤醒一个M与这个P绑定,进而P中剩下的goroutine可以得到执行

sysmon_20211203183126.png

retake_20211203183227.png

golang调试

golang程序调试工具有gdb、godebug、delve等,gdb原生支持c/c++程序,对golang语法支持比较差,godebug专为golang调试开发,目前功能尚不完善,不支持attach等,delve工具可以说是golang的gdb,支持golang在调试模式下运行、attach、调试core等,所以目前采用delve工具调试golang是最佳选择

  • delve安装

delve项目地址:github.com/go-delve/delve/,克隆到GOPATH目录下,然后执行make install,最后把dlv所在目录加到PATH路径即可

由于delve对golang的版本有要求,我的golang版本是1.8.3,编译最新的delve出错,所以尝试几个老版本,亲测devle的1.2.0版本可正常安装,所以需要checkout到1.2.0,然后再编译安装

  • delve使用

delve的帮助与gdb类似,dlv --help 可查看delve有哪些子命令,具体子命令用法例如debug,可输入dlv debug --help
dlv debug 可直接调试golang源文件
dlv exec 调试golang二进制程序
dlv attach 可对正在运行的程序截取快照进行调试
dlv core 可调试崩溃文件

  • golang崩溃

go程序在崩溃时,其崩溃所在的goroutine会执行每个栈帧对应的defer函数,若执行recover函数获取panic信息,可避免进程异常退出,仅仅其所在的goroutine退出,但通常情况下程序出现异常是需要进程退出,否则会出现未知逻辑错误

在程序异常退出时,若在defer函数中采用runtime.Stack()获取栈信息,则需要在每个goroutine中都要加defer函数,比较麻烦,若截获SIGSEGV等异常信号,golang又不像c语言那样可设定每个信号的handler,一旦异常信号出现采用golang的notify机制却捕获不到,即使采用defer或者c语言的handler方式,采集到的栈信息已经不在崩溃点,所以异常信息只能采用coredump来解决,而golang在崩溃时,默认仅产生当前goroutine的崩溃信息,并没有产生core文件,因此需要修改其运行时环境,让其产生core文件

  • GOTRACEBACK

GOTRACEBACK是golang运行时环境变量之一,用于控制go程序崩溃时的处理,有以下几种取值

GOTRACEBACK=none,仅产生panic信息,不产生栈信息
GOTRACEBACK=single,在none基础上,产生当前goroutine的栈信息
GOTRACEBACK=all,在none基础上,产生所有goroutine的栈信息
GOTRACEBACK=system,在all基础上,产生包括runtime的栈信息
GOTRACEBACK=core,在system基础上,产生core dump文件

默认是single,所以仅需要执行 env GOTRACEBACK=core ./xxx.exe,即可产生core文件,若采用sudo方式运行程序,由于sudoer账户在运行程序前会重置当前用户环境变量,所以需要修改/etc/sudoers文件,注释掉Defaults env_reset,添加Defaults !env_reset
更多运行时环境变量可参考https://dave.cheney.net/2015/11/29/a-whirlwind-tour-of-gos-runtime-environment-variables

系统默认产生core文件大小限制为0,即禁止产生core文件,需执行ulimit -c unlimited,若需要修改core文件名称为程序名.core.pid,则需要修改/etc/sysctl.conf文件,在文件后追加kernel.core_pattern =%e.core.%p
kernel.core_uses_pid = 0
然后执行sysctl -p生效

golang 源码编译安装

在version<=1.4,golang的所有组件都是用c+asm写的,之后的版本是用go+asm写的,所以安装早期的版本只需要gcc即可,但是后期版本的安装需要依赖早期的版本,下面分别从1.4版本和1.8.3两个版本分别介绍其源码安装过程

  • 1.4版本

    cd ~/package
    wget https://storage.googleapis.com/golang/go1.4-bootstrap-20170531.tar.gz
    tar zxvf go1.4-bootstrap-20170531.tar.gz go1.4
    cd go1.4/src/
    CGO_ENABLED=0 ./all.bash

    若只想使用1.4版本,此时可以在~/.bash_profile中添加export PATH=$PATH:~/package/go1.4/bin

  • 1.8.3版本

    cd ~/package
    wget https://github.com/golang/go/archive/go1.8.3.tar.gz
    tar zxvf go1.8.3.tar.gz go1.8.3
    export GOROOT_BOOTSTRAP=~/package/go1.4
    cd go1.8.3/src
    ./all.bash

    若想用1.8.3版本,可以把~/bash.profile文件中的go1.4改成go1.8.3即可