go 1.12.7
文中未标明包名之名称均在 runtime
包中
interface
type iface struct {
type eface struct {
- 非空接口类型
iface
结构体包含:tab *itab
data unsafe.Pointer
- 空接口类型(即
interface{}
类型)eface
结构体包含:_type *_type
data unsafe.Pointer
-
itab
结构体包含:-
hash uint32
用于在接口和具体类型转换时判定类型是否相符 -
inter *interfacetype
接口类型的信息 -
_type *_type
具体类型的信息 -
fun [1]uintptr
用作虚表,直接使用该字段的指针,用作一个变长数组
-
- 具体类型转为接口,
convT2I
函数:- 在编译时就已构造好
*itab
结构 - 传入
*itab
和具体类型的指针 - 分配具体类型大小的空间,将
data
字段指向空间,将具体类型拷贝到空间
- 在编译时就已构造好
- 接口类型转为接口,
convI2I
函数:- 传入转换后接口类型的
*interfacetype
和转换前的iface
- 如果转换后的
*interfacetype
和转换前的iface.tab.inter
相等,则前后iface
的两个字段直接拷贝 - 否则,检查具体类型是否实现转换后的接口,特别是构造虚表,此过程的结果会缓存
- 传入转换后接口类型的
slice
type slice struct {
- range 接收的是被 range 对象的按值传递,因此在一个对 slice 的 range 中对 slice 进行 append 不会造成无限循环
- 扩容,
growslice
函数:- 如果新长度大于当前容量的两倍,就扩容至新长度
- 否则如果当前容量小于 1024,就将容量翻倍
- 否则,循环增加当前容量的 1/4 直到大于等于新长度
map
type hmap struct {
-
hmap
结构体包含:-
count int
键值对的个数 -
hash0 uint32
求哈希值的随机种子 -
B uint8
桶的数量的以 2 为底的对数,桶的数量总是 2 的整次幂 -
buckets unsafe.Pointer
当前桶的指针,指向 malloc 出的连续多个桶的第一个 -
oldbuckets unsafe.Pointer
扩容之前的桶的指针
-
- 创建 map,
makemap
函数:根据make
函数传入的 hint 来创建初始的 2 的整次幂个桶。桶的结构为bmap
,但bmap
真正的字段结构是在运行时创建的,而非代码中声明的结构,因为 key 有不同的类型,而 golang 没有泛型!这也是为什么buckets
字段的类型为unsafe.Pointer
而非*bmap
- 逻辑上
bmap
结构体包含:-
topbits [8]uint8
缓存每个 key 的哈希值的高 8 位 keys [8]K
-
values [8]V
可见每个桶最多存 8 个键值对
-
- range 遍历,
mapiterinit
函数:生成一个随机数来决定从哪个桶开始遍历 -
m[k]
式的读操作,mapaccess1
函数:计算uintptr
类型的哈希值,通过哈希值的低B
位确定访问哪个桶,通过哈希值的高 8 位与桶中的topbits
逐一对比,相同时再进行 key 的对比 - 写操作,
mapassign
函数:与m[k]
式的读相似地通过哈希值查找,如果发现 key 不存在则写入到第一个空的点位。如果键值对数量与容量的比值大于 6.5/8 且未在扩容中则开始扩容 - 扩容,
hashGrow
函数:将buckets
赋给oldbuckets
,给buckets
分配翻倍的桶数,原第i
个桶的数据会迁移到新第i
和i + len(oldbuckets)
个桶。每个桶的数据迁移是在该桶涉及到写和删操作时进行的(growWork
函数)
func
- 与 C 语言使用寄存器不同,Golang 只使用栈进行函数参数和返回值的传递,被调函数的参数和返回值存放在主调函数的栈帧上,这也是支持多值返回的原因
- Plan9 寄存器:
- 通用寄存器
AX ~ DX, DI, SI, BP, SP, R8 ~ R14, PC
- 伪寄存器:
-
FP
:主调函数(上一个栈帧)中对当前函数调用的参数的起始(最低)地址,使用形式为symbol+offset(FP)
,如arg1+8(FP)
,offset
只是关于FP
的偏移,symbol
只是一个增强可读性的标记 -
SP
:当前函数局部变量的起始(最高)地址,使用形式为symbol-offset(SP)
,offset
只是关于FP
的偏移,symbol
只是一个增强可读性的标记。与通用寄存器SP
的区分方式为,不带symbol+
形式的为通用寄存器 -
SB
:全局区起始(最低)地址,使用形式为symbol+offset(SB)
,offset
是关于symbol
的偏移
-
- 通用寄存器
- 栈帧结构(主调函数 -> 当前函数 -> 被调函数):
- 当前函数的栈帧从地址
b
开始向低地址发展 -
b-1
到b-8
存放主调函数的BP
,伪 SP
和 当前BP
指向b-8
-
b-9
到c
存放局部变量 var0 到 varN -
c
到d
存放被调函数的返回值 retN 到 ret0 和 参数 argN 到 arg0,伪 FP
和 当前SP
指向d
-
d-1
到d-8
存放被调函数返回时需要回到的PC
- 当前函数的栈帧从地址
defer
type _defer struct {
-
_defer
结构体包含:-
sp uintptr
当前函数的栈指针 -
pc uintptr
当前函数的程序指针 -
fn *funcval
传给 defer 的函数
-
- defer 语句执行,
deferproc
函数:设置以上字段,传递 defer 函数的参数,将_defer
结构体置于当前协程的
_defer
结构体组成的链表的头部 - 编译时会插入代码,在当前函数返回时,遍历执行链表中所有栈指针与当前函数栈指针相同的
_defer
结构体中的函数(deferreturn
函数)
goroutine
type m struct {
type g struct {
type p struct {
- 操作系统线程
m
结构体包含:-
g0 *g
拥有调度栈的调度协程 -
curg *g
当前运行的协程 -
p uintptr
绑定的调度器
-
- 协程
g
结构体包含:-
m *m
绑定的线程 -
sched gobuf
寄存器等上下文 atomicstatus uint32
-
- 协程状态
atomicstatus
:-
_Gidle
未初始化 -
_Gdead
未运行,不在队列中 -
_Grunnable
未运行,在队列中等待调度 -
_Grunning
正运行在用户态,不在队列中 -
_Gsyscall
正运行在内核态,不在队列中 -
_Gwaiting
被阻塞,不在队列中
-
- 调度器
p
结构包含:-
m uintptr
绑定的线程 -
runq [256]uintptr
待运行协程的队列,数组用作一个循环队列 -
runnext uintptr
下一个运行的协程结构体的指针 -
gFree ...
状态为_Gdead
的空闲协程结构的队列
-
-
GOMAXPROCS
个线程运行在用户态,默认为 CPU 核数。一个线程绑定一个调度器。同时存在一个全局待运行协程队列sched.runq
- go 关键字执行,
newproc
函数:- 从当前线程的调度器的空闲队列中获取一个协程结构,如果没有就新建一个并分配栈空间
- 将入口函数的参数整片拷贝到新协程的栈中
- 新协程加入队列,
runqput
函数:新协程的状态置为_Grunnable
,特权式地添加到调度器中,协程的指针直接设置至runnext
字段。如果队列已满,将之前的runnext
发配到全局队列
- 协程暂停,
gopark
函数:- 切换至调度协程
g0
,mcall
函数:汇编实现,保存当前协程程序指针、栈指针,设置 CPU 寄存切换至调度协程 - 处理当前协程,
park_m
函数:当前协程状态置为_Gwaiting
- 选择下一协程,
schedule
函数:- 如果需要执行 gc 标记任务,选择一个当前线程调度器中的 gc 标记任务协程
- 否则一定几率从全局队列获取
- 否则从当前线程的调度器获取:如果
runnext
不为空,则选择它,否则选择队列头部 - 否则,调用
findrunnable
从其它调度器、全局队列、epoll 中获取,直到获取到一个才会返回
- 执行下一协程,
execute
函数:状态置为_Grunning
,建立与线程的关系,在汇编实现的gogo
函数中设置 CPU 寄存切换至下一协程
- 切换至调度协程
- 系统调用,
syscall.Syscall
函数:- 进入,
entersyscall
函数:保存当前的程序指针、栈指针,当前协程状态置为_Gsyscall
,解除调度器与线程的绑定,线程陷入内核态 - 退出,
exitsyscall
函数:线程重新绑定调度器
- 进入,
channel/select
type hchan struct {
-
hchan
结构体包含:-
buf unsafe.Pointer
缓冲区队列 -
qcount uint
缓冲区的长度 -
dataqsiz uint
缓冲区的容量,因为是一个循环队列 -
sendx uint
写到哪一个下标 -
recvx uint
读到哪一个下标 -
elemtype *_type
缓冲区元素的类型信息 -
elemsize uint16
缓冲区元素的大小 -
sendq waitq
因为写而阻塞于此的协程列表,元素类型为*sudog
,双向链表 -
recvq waitq
因为读而阻塞于此的协程列表
-
- 创建 channel,
makechan
函数:如果无缓冲器,则不为buf
分配空间;否则如果元素不为指针类型,则为buf
分配空间和hchan
连续的空间;否则为buf
分配独立的空间 - 向 channel 发送,
chansend1
函数:- 如果 channel 为
nil
,则协程永远阻塞 - 如果 channel 已关闭,则 panic
- 如果有因为读而阻塞于此的协程,
send
函数:- 将接收方的
sudog
移出recvq
- 将消息拷贝在
sudog
中的接收变量的地址 - 将接收方协程的状态从
_Gwaiting
置为_Grunnable
,同样特权式地将协程插入到调度器的队列 - 发送方协程不会阻塞,状态始终是
_Grunning
- 将接收方的
- 否则如果 channel 有缓冲区且未满,则将消息拷贝到缓冲区尾部
- 否则,阻塞发送:
- 将当前协程以及发送变量的指针存入
sudog
,将sudog
加入sendq
- 调用
gopark
暂停当前协程 - 等接收操作到来时,此协程会被重新调度
- 将当前协程以及发送变量的指针存入
- 如果 channel 为
- 从 channel 接收,
chanrecv1
函数:- 如果 channel 为
nil
,则协程永远阻塞 - 如果 channel 已关闭且缓冲区为空,则将接收变量置零并返回
- 如果有因为写而阻塞于此的协程,
recv
函数:- 将发送方的
sudog
移出sendq
- 将消息从在
sudog
中的发送变量的地址拷贝,如果有缓冲区且不空,则将消息拷贝到缓冲区尾部,将头部出队并拷贝到接收变量,否则将消息拷贝到接收变量 - 将发送方协程的状态从
_Gwaiting
置为_Grunnable
,同样特权式地将协程插入到调度器的队列 - 接收方协程不会阻塞,状态始终是
_Grunning
- 将发送方的
- 否则如果 channel 有缓冲区且不空,则将头部出队并拷贝到接收变量
- 否则,阻塞接收,过程与发送对偶
- 如果 channel 为
- 关闭 channel,
closechan
函数:- 如果 channel 为 nil 或已关闭,则 panic
- 所有
sendq
和recvq
中的协程的状态从_Gwaiting
置为_Grunnable
,同样特权式地将协程插入到调度器的队列。recvq
中的协程的接收会收到零值,sendq
中的协程的发送会 panic
- select 块中没有任何 case 或 default,则协程永远阻塞
- select 块中只有一个 case 且没有 default,则退化为没有 select 的单个 channel 操作
- select 块中只有一个 case 和一个 default:
- 退化为 if case else default 的执行
- case 的 channel 操作执行
selectnbsend
/selectnbrecv
函数:- 与
chansend1
/chanrecv1
的差别仅在于:- 如果 channel 为
nil
则返回 - 在最后的阻塞操作之前返回
- 如果 channel 为
- 当且仅当以上情况返回
false
作为 if 的条件
- 与
- select 块中为其他情况时,通过
selectgo
函数确定一个执行分支:- 随机确定 case 的遍历考察顺序
- 如果 channel 为
nil
,下一个 - 如果是写操作:
- 如果 channel 已关闭,则 panic
- 如果有因为读而阻塞于此的协程,
send
函数 - 否则如果 channel 有缓冲区且未满,则将消息拷贝到缓冲区尾部
- 以上情况会确定执行此 case,否则下一个
- 如果是读操作:
- 如果有因为写而阻塞于此的协程,
recv
函数 - 否则如果 channel 有缓冲区且不空,则将头部出队并拷贝到接收变量
- 否则如果 channel 已关闭,则将接收变量置零
- 以上情况会确定执行此 case,否则下一个
- 如果有因为写而阻塞于此的协程,
- 如果 channel 为
- 如果没有选择一个 case 作为执行分支,则执行 default
- 如果没有 default:
- 加入到所有 channel 的
sendq
或recvq
- 调用
gopark
暂停当前协程 - 等接收或发送操作到来时,此协程会被重新调度,并离开所有加入的
sendq
或recvq
- 加入到所有 channel 的
- 随机确定 case 的遍历考察顺序
gc
- 非分代,非紧凑,三色标记,写屏障
- 三色标记:
- 黑:已标记,子对象已考察,不在队列中
- 灰:已标记,子对象待考察,在队列中
- 白:未标记
- 触发:
- 堆:使用量达到动态计算的阈值,
mallocgc
函数 - 时间:上次 gc 后达到两分钟,
forcegchelper
函数 - 主动:
GC
函数
- 堆:使用量达到动态计算的阈值,
- 总体过程:
- 进入标记阶段,
gcStart
函数:- 获取
worldsema
信号量 - 确保每个线程当中都有一个执行标记任务的协程
gcBgMarkWorker
- STW!
stopTheWorldWithSema
函数 -
gcphase
从_GCoff
置为_GCmark
,启动写屏障 - 统计 root 区块数量,
gcMarkRootPrepare
函数 - start the world,
startTheWorldWithSema
函数
- 获取
- 标记:
- 触发:如前文所描述,
schedule
函数选择运行一个标记任务协程 - 处理所有灰色对象,
gcDrain
函数:- 寻址到一个 root,标记 root,
markroot
函数:- 将对象标灰加入队列,
greyobject
函数
- 将对象标灰加入队列,
- 选择一个灰色对象移出队列,将该对象所引用的对象标灰加入队列,
scanobject
函数
- 寻址到一个 root,标记 root,
- 触发:如前文所描述,
- 标记完成,进入清扫阶段,
gcMarkDone
函数:- 触发:在
gcBgMarkWorker
中调用 - STW!
-
gcphase
置为_GCmarktermination
- 处理写屏障的记录,
wbBufFlush1
函数:将写屏障记录的,在标记阶段发生变化而遗漏的对象标记,必须在 STW 之下进行,否则此时可能又有变化,将无限循环 - 唤醒清扫任务协程,
gcSweep
函数:调用ready
函数将sweep.g
的状态置为_Grunnable
加入调度器队列 -
gcphase
置为_GCoff
,关闭写屏障 - start the world
- 释放
worldsema
信号量
- 触发:在
- 清扫:
- 触发:如前文所描述,
gcSweep
唤醒清扫任务协程 - 释放一个申请的堆空间,
sweepone
函数
- 触发:如前文所描述,
- 进入标记阶段,
tcp (on Linux)
-
net.ListenTCP
函数:-
net.ListenTCP
->net.sysListener.listenTCP
->net.internetSocket
->net.socket
->net.netFD.listenStream
- 在
net.socket
->net.sysSocket
中调用系统调用socket
新建监听套接字的文件描述符 - 在
net.netFD.listenStream
方法中监听端口:- 调用系统调用
bind
和listen
- 在
internal/poll.pollDesc.init
方法中处理 epoll:- 在
poll_runtime_pollServerInit
函数中调用 Linux APIepoll_create1
创建 epoll 的文件描述符,赋予全局变量epfd
,整个进程中只调用一次 - 在
poll_runtime_pollOpen
函数中调用 Linux APIepoll_ctl(EPOLL_CTL_ADD)
将监听套接字注册到 epoll
- 在
- 调用系统调用
-
-
net.TCPListener.AcceptTCP
方法:-
net.TCPListener.AcceptTCP
->net.TCPListener.accept
->net.netFD.accept
- 在
net.netFD.accept
方法中获取一个连接套接字:- 在
internal/poll.FD.Accept
方法中获取一个连接套接字:- 调用系统调用
accept4
- 如果获取不到,在
poll_runtime_pollWait
函数中调用gopark
函数暂停协程。如前文所描述,在findrunnable
/startTheWorldWithSema
等调度过程中,可能调用netpoll
函数,在其中调用 Linux APIepoll_wait
获取 IO ready 的监听套接字,将其对应的协程恢复执行
- 调用系统调用
- 在
internal/poll.pollDesc.init
方法中处理 epoll:- 在
poll_runtime_pollOpen
函数中调用 Linux APIepoll_ctl(EPOLL_CTL_ADD)
将连接套接字注册到 epoll
- 在
- 在
-
-
net.conn.Write
/net.conn.Read
方法:- 调用系统调用
write
/read
- 如果阻塞,如前文所描述调用
poll_runtime_pollWait
暂停协程,且在netpoll
函数中恢复执行 IO ready 的连接套接字对应的协程 -
net.netFD
->internal/poll.FD
->internal/poll.pollDesc
->pollDesc
,一个pollDesc
结构中分别包含一个写与读阻塞于此的协程的指针,即写与读的阻塞是分开的
- 调用系统调用
Licensed under CC BY-SA 4.0