Golang 运行时学习笔记

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 个桶的数据会迁移到新第 ii + 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-1b-8 存放主调函数的 BP伪 SP 和 当前 BP 指向 b-8
    • b-9c 存放局部变量 var0 到 varN
    • cd 存放被调函数的返回值 retN 到 ret0 和 参数 argN 到 arg0,伪 FP 和 当前 SP 指向 d
    • d-1d-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 函数:
    • 切换至调度协程 g0mcall 函数:汇编实现,保存当前协程程序指针、栈指针,设置 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 接收,chanrecv1 函数:
    • 如果 channel 为 nil,则协程永远阻塞
    • 如果 channel 已关闭且缓冲区为空,则将接收变量置零并返回
    • 如果有因为写而阻塞于此的协程,recv 函数:
      • 将发送方的 sudog 移出 sendq
      • 将消息从在 sudog 中的发送变量的地址拷贝,如果有缓冲区且不空,则将消息拷贝到缓冲区尾部,将头部出队并拷贝到接收变量,否则将消息拷贝到接收变量
      • 将发送方协程的状态从 _Gwaiting 置为 _Grunnable,同样特权式地将协程插入到调度器的队列
      • 接收方协程不会阻塞,状态始终是 _Grunning
    • 否则如果 channel 有缓冲区且不空,则将头部出队并拷贝到接收变量
    • 否则,阻塞接收,过程与发送对偶
  • 关闭 channel,closechan 函数:
    • 如果 channel 为 nil 或已关闭,则 panic
    • 所有 sendqrecvq 中的协程的状态从 _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 则返回
        • 在最后的阻塞操作之前返回
      • 当且仅当以上情况返回 false 作为 if 的条件
  • select 块中为其他情况时,通过 selectgo 函数确定一个执行分支:
    • 随机确定 case 的遍历考察顺序
      • 如果 channel 为 nil,下一个
      • 如果是写操作:
        • 如果 channel 已关闭,则 panic
        • 如果有因为读而阻塞于此的协程,send 函数
        • 否则如果 channel 有缓冲区且未满,则将消息拷贝到缓冲区尾部
        • 以上情况会确定执行此 case,否则下一个
      • 如果是读操作:
        • 如果有因为写而阻塞于此的协程,recv 函数
        • 否则如果 channel 有缓冲区且不空,则将头部出队并拷贝到接收变量
        • 否则如果 channel 已关闭,则将接收变量置零
        • 以上情况会确定执行此 case,否则下一个
    • 如果没有选择一个 case 作为执行分支,则执行 default
    • 如果没有 default:
      • 加入到所有 channel 的 sendqrecvq
      • 调用 gopark 暂停当前协程
      • 等接收或发送操作到来时,此协程会被重新调度,并离开所有加入的 sendqrecvq

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 函数
    • 标记完成,进入清扫阶段,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 方法中监听端口:
      • 调用系统调用 bindlisten
      • internal/poll.pollDesc.init 方法中处理 epoll:
        • poll_runtime_pollServerInit 函数中调用 Linux API epoll_create1 创建 epoll 的文件描述符,赋予全局变量 epfd,整个进程中只调用一次
        • poll_runtime_pollOpen 函数中调用 Linux API epoll_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 API epoll_wait 获取 IO ready 的监听套接字,将其对应的协程恢复执行
      • internal/poll.pollDesc.init 方法中处理 epoll:
        • poll_runtime_pollOpen 函数中调用 Linux API epoll_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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容