Alan Hou的个人博客

跟着曹大学Golang第一回 – Go 程序是怎么跑起来的

暖场内容

跨语⾔学习

PHP 转 Go

Python 转 Go

⼯程师的学习与进步

工程师应该怎么学习

多写代码,积累代码量(⾄少积累⼏⼗万的代码量,才能对设计有⾃⼰的观点),要总结思考,如何对过去的⼯作进⾏改进(如⾃动化/系统化);积累⾃⼰的代码库笔记库开源项⽬

读好书,建⽴知识体系(⽐如像 Designing Data-Intensive Application,中文GitHub版:设计数据密集型应用这种书,应该读好多遍)。

关注⼀些靠谱的国内外新闻源,通过问题出发,主动使⽤ Google,主动去 reddit、hackernews 上参与讨论,避免被困在信息茧房中。

锻炼⼝才演讲能⼒,内部分享 -> 外部分享。在公司内,该演要演,不要只是闷头⼲活。

通过输出促进输⼊(博客、公众号、分享),打造个⼈品牌,通过读者的反馈循环提升⾃⼰的认知。(费曼学习法

信息源Github TrendingRedditMediumHacker Newsmorning paper(作者不⼲了),acm.orgO’Reilly,国外的领域相关⼤会(如 OSDI,SOSP,VLDB)论⽂,国际⼀流公司的技术博客,YouTube 上的国外⼯程师演讲。

为什么 Go 语⾔适合现代的后端编程环境?

为什么适合基础设施?

对 Go 的启动和执⾏流程建⽴简单的宏观认识

理解可执⾏⽂件

实验使用的 Dockerfile(鉴于 CentOS 8已正式停止维护,可将 centos 镜像替换为 rockylinux)

构建镜像和启动容器进入的命令

vim tab 空格数的配置(~/.vimrc)

Go 程序 hello.go 的编译过程:

可执行go build -x hello.go进行查看,代码中高亮的两行分别为 complie(编译)和 link(链接)部分:

编译是将文本代码编译为目标文件(.o, .a),链接是将目标文件合并为可执行文件。可执行文件在不同的操作系统上规范也不同:

LinuxWindowsmacOS
ELFPEMach-O

Linux 的可执⾏⽂件 ELF(Executable and Linkable Format) 为例,ELF 由⼏部分构成:

图片来源:https://github.com/corkami/pics/blob/28cb0226093ed57b348723bc473cea0162dad366/binary/elf101/elf101.pdf

图片来源:https://github.com/corkami/pics/blob/28cb0226093ed57b348723bc473cea0162dad366/binary/elf101/elf101.pdf

操作系统执⾏可执⾏⽂件的步骤(以 linux 为例):

通过 entry point 找到 Go 进程的执⾏⼊⼝,使⽤ readelf(readelf -h ./hello

通过以上的入口地址0x45cd80可借助dlv命令来设置断点找到代码位置(事实上进入后默认已经在入口位置):

常用的命令:c, si, r, n, disass,更多参见https://github.com/go-delve/delve

 

Go 进程的启动与初始化

有助于深入汇编的游戏:人力资源机器(Human Resource Machine

CPU 无法理解文本,只能执行一条一条的二进制机器码指令 每次执行完一条指令,pc 寄存器就指向下一条继续执行

在 64 位平台上 pc 寄存器 = rip

Go 语⾔是⼀⻔有 runtime 的语⾔,runtime 是为了实现额外的功能,⽽在程序运⾏时⾃动加载/运⾏的⼀些模块。(下图仅供参考,不够严谨,线程创建在 Linux 中也是通过系统调用)

Go 语⾔的 runtime 包括如下模块:

Go 语⾔的 runtime 包括如下模块:
Scheduler调度器管理所有的 G,M,P,在后台执⾏调度循环
Netpoll⽹络轮询负责管理⽹络 FD 相关的读写、就绪事件
Memory
Management
当代码需要内存时,负责内存分配⼯作
Garbage
Collector
当内存不再需要时,负责回收内存

这些模块中,最核⼼的就是 Scheduler,它负责串联所有的 runtime 流程。

通过 entry point 找到 Go 进程的执⾏⼊⼝,它会通过3个回调函数一路跳到runtime.rt0_go这个函数里,该函数是初始化的一个非常重要的函数。下图中m0为Go 程序启动后创建的第⼀个线程,执行 main 函数后开始进入调试循环:

调度组件与调度循环

在写下go func(){...}的时候,其实是向 runtime 提交了⼀个计算任务。 func(){...}⾥包裹的代码,就是这个计算任务的内容。所以Go 的调度流程本质上是⼀个⽣产-消费流程:

go func去了哪⾥?

goroutine 的生产端

生产端点击这里通过动画进行理解

goroutine 的消费端

M 执行调度循环时, 必须与一个 P 绑定。Work stealing 就是说的 runqsteal -> runqgrab 这个流程。

每执行60次本地队列的获取(可参见下图中使用的是一个魔法数字61),就会去全局队列中检查一次

下图中P.schedtick即为记录前面本地队列获取的次数

消费端点击这里通过动画进行理解

使用是 M:N 模型?关于 GMP 的说明:

处理阻塞

在线程发生阻塞的时候, 会无限制地创建线程么?

并不会!! 先来看看阻塞有哪几种情况:

1. make 了一个 buffer 是0的 channel,向里面塞数据:

2.make 了一个 buffer 是0的 channel,去消费数据:

3.执行time.sleep

4.网络读,但无数据可读

5.网络写,缓冲区已满

6.执行 select,但 case 均为 ready

7.锁被其他人占用

这些情况不会阻塞调度循环,而是会把 goroutine 挂起。所谓的挂起,其实让 g 先进某个数据结构,待 ready 后再继续执行,不会占用线程。这时候,线程会进入 schedule,继续消费队列,执行其它的 g

上述6种情况挂起的示意:

为何有的等待是 sudog,有的是 g呢?

就是说一个 g 可能对应多个 sudog,比如一个 g 会同时 select 多个 channel。前面这些都是能被 runtime 拦截到的阻塞,还有一些是 runtime 无法拦截的:

1. cgo:在执行 c 代码,或者阻塞在 syscall 上时,必须占用一个线程

2. syscall

sysnb: syscall nonblocking sys: syscall blocking

处理是通过sysmon(system monitor),具有高优先级,在专有线程中执行,不需要绑定 P 就可以执行。主要有3个工作

调度器的发展历史

参见:https://github.com/golang-design/history#scheduler

小结

可执行文件 ELF:

启动流程:

Runtime 构成:

GMP:

队列:

调度循环:

处理阻塞:

sysmon:

补充:与调度有关的常⻅问题

Goroutine 比 Thread 优势在哪?

GoroutineThread
内存占用2KB -> 1GB从 8k 开始,服务端程序上限很多是 8M(用 ulimit -a 可看),调用多会 stack overflow
Context switch几十 NS 级1-2 us
由谁管理Go runtime操作系统
通信方式CSP/传统共享内存传统共享内存
ID有,用户无法访问
抢占1.13 以前需主动让出 1.14 开始可由信号中断内核抢占

参考链接:https://www.geeksforgeeks.org/golang-goroutine-vs-thread/

goroutine 的切换成本

gobuf 描述一个 goroutine 所有现场,从一个 g 切换到另一个 g,只要把这几个现场字段保存下来,再把 g 往队列里一扔,m 就可以执行其它 g 了。无需进入内核态

一个无聊的输出顺序的问题

第一段代码:

第二段代码:

死循环导致进程 hang 死问题

GC 时需要停止所有 goroutine 而老版本的 Go 的 g 停止需要主动让出

1.14 增加基于信号的抢占之后,该问题被解决

链接:https://xargin.com/how-to-locate-for-block-in-golang/

与 GMP 有关的一些缺陷

在 PPT 里有各种阻塞场景,你是怎么在代码里找到这些阻塞场景的?

要知道 runtime 中可以接管的阻塞是通过 gopark/goparkunlock 挂起和 goready 恢复 的,那么我们只要找到 runtime.gopark 的调用方,就可以知道在哪些地方会被 runtime 接管了,你也应该用 IDE 试一试,很简单:

其它参考链接:

Measuring context switching and memory overheads for Linux threads

课后作业

内容来源为曹大的《Go高级工程师实战营》,想要报名的小伙伴请访问https://learn.gocn.vip/course(无偿广告,内容是否适合读者请自行评估)。

退出移动版