golang系列之-程序运行流程及GMP模型
本文仅介绍程序运行流程以及GMP如何寻找g并运行,其他如抢占、死锁、信号处理、profiling等内容不打算深入。当前go版本:1.24
前提
先讲几个概念
进程、线程、协程
- 进程:程序的一个实例,也是操作系统的一个task,操作系统的资源分配最小单位
- 线程:一种概念,操作系统调度的最小单位,一个进程可以包含多个线程,线程之间共享内存、文件描述符等资源。不同操作系统的实现并不一致,linux下进程与线程的结构都是task_structure,也就是说他们是一样的,不同线程之间用指针指向同一份资源如内存空间实现共享。
- 协程:也称为用户态线程,由应用程序自行实现/调度,一般情况下是协作式的,由开发者决定task何时让出cpu,go支持抢占式调度
线程与协程的映射模型
- 1:1模型,每个用户线程对应一个内核线程,如在c中pthread创建的线程,此时也可以理解为用户态线程就是内核态线程
- 1:N模型,一个内核线程对应多个用户线程,无法充分利用多核的并行性,现已淘汰
- M:N模型,多个用户线程对应多个内核线程,实现较复杂,使用者较少,go是其中一个
GMP模型
go早期的M:N模型遇到一些性能问题,如锁竞争激烈、线程创建/销毁频繁、CPU缓存失效等,为了解决这些问题引入了P。在GMP模型中
- G-goroutine,用户态线程
- M-machine,系统线程相关
- P-processor,缓存、调度上下文等,其数量一般与CPU核心数量一致。P的出现使得调度变得本地化,避免全局锁竞争,提升了CPU缓存命中率等,最终使得go的并发调度更加高效
go程序的运行流程
go程序启动时的入口是_rt0_amd64
,该函数是汇编代码,具体如下
1 | // src/runtime/asm_amd64.s |
runtime·rt0_go
也是汇编代码,比较长,主要逻辑如下
- g0、m0双向绑定(g0、m0是全局变量,静态编译,因此指针已知,放在src/runtime/proc.go)
- runtime·args - 复制命令行参数(args函数放在src/runtime/runtime1.go)
- runtime·osinit - 系统初始化(osinit函数放在src/runtime/os_linux.go)
- runtime·schedinit - 调度器初始化(schedinit函数放在src/runtime/proc.go)
- runtime·mainPC - 纪录runtime·main的地址(main函数放在src/runtime/proc.go,其内部调用main_main,也就是用户自己编写的main函数)
- runtime·newproc - 创建G用于运行runtime·main,放到p的runq里,等待调度
- runtime·mstart - 运行runtime·mstart(汇编函数,实际调用的是runtime·mstart0,放在src/runtime/proc.go,内部进行栈初始化、信号注册等,最后运行调度函数schedule)