分类 语言 下的文章

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

语言解释器的实现

出于个人情怀和早期对语言实现的兴趣,最近业余时间写了个简化版语言解释器,性能是python的3倍左右,几乎包含语言所有的特性,下面主要讲一下核心实现,感兴趣的可以直接看golang源码:https://github.com/song2010040402102/cy

词法分析

词法分析主要对一个表达式进行分词,把各个变量和运算符拆分到一个array中,遇到()或function,把它当成子表达式递归处理即可

语法分析

语法分析相对词法分析较为复杂,首先需要把词法array中的运算符中同名不同含义的进行转化,例如,-既可以作为一元运算符负号也可以作为二元运算符减号,++要分析是先赋值还是先自增,然后根据运算符的优先级构造语法树,其中叶子节点存储变量,其它节点存储运算符,而每个子节点的数量取决于运算符的元数,对于一元运算符要根据结合方向来选择左边还是右边的表达式作为子节点。为了通用起见,可以定义运算符的优先级、结合性、元数等

语义分析

语义分析对于自然语言非常复杂,对于计算机语言可以简单理解为像c语言编译器在编译过程报的错误或警告,即处理变量类型是否可转换或运算符是否可操作当前的变量类型等等。为了通用起见,可以定义一个变量和运算符的语义表

符号表

符号表用来表示每个变量的名称和类型,在程序运行时,用于申请堆栈的大小

符号表又分为全局符号表和函数符号表,在符号查找时,优先查找函数符号表,查找失败再查找全局符号表

由于函数可能会嵌套一些条件和循环语句,所以函数符号表是个树形结构,应该沿着当前节点往树根方向查找

指令生成

程序运行时,不可能沿着语法树来执行,会大大降低执行效率,因此需要把语法树转换成线性指令,转换过程只需要深度优先遍历即可

控制语句

像if、while、continue、break、return等都可抽象成运算符来处理,if运算符逻辑决定执行哪个语句块,while运算符逻辑决定是否要循环执行语句块,continue、break、return通过改变程序计数器来控制当前执行哪个指令

以上六个部分是解释器的核心,其他像变量、函数声明,堆栈空间申请与释放,ebp、esp、eax、pc等寄存器使用都相对比较简单,不再赘述

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生效

go语言致命的bug

通常运算符的优先级大体是算术>关系>逻辑>赋值,但按位取与运算符本应划为算术运算符,但c语言应该把它划为逻辑运算符,导致按位取与运算符的优先级低于关系运算符,这是很多c语言初学者最容易犯的错误,但go语言又一反常态,把按位取与运算符划为算术运算符,导致按位取与运算符的优先级高于关系运算符,因为大部分语言的运算符优先级都是参照c语言的,导致很多从其它语言转向go语言的程序员在写go程序时,尤其不喜欢加括号爱装逼的,很容易犯这样一个致命错误!