go 1.12.7
文中未标明包名之名称均在 runtime 包中
interface
type iface struct {
type eface struct {
- 非空接口类型
iface结构体包含:tab *itabdata unsafe.Pointer
- 空接口类型(即
interface{}类型)eface结构体包含:_type *_typedata 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