Go1.25的源码分析-src/runtime/runtime1.go(GMP)g
1. 主要组成部分
Go语言的GMP调度器基于四个核心数据结构:g
、m
、p
和schedt
。
1.1 主要常量解读
1.1.1G 状态常量
const (_Gidle = iota //刚分配尚未初始化的 G_Grunnable//已在运行队列上,未执行用户代码;栈未被该 G 拥有_Grunning//正在执行用户代码;已分配 M 与 P;不在运行队列上;栈由该 G 拥有_Gsyscall//正在执行系统调用;不执行用户代码;不在运行队列上;已分配 M;栈由该 G 拥有_Gwaiting//在 runtime 内部阻塞(如通道、锁、定时器等);不执行用户代码;不在运行队列上;通常不拥有栈(特定通道路径在锁下可读写)_Gmoribund_unused//预留给调试器(gdb)_Gdead//当前未使用的 G(刚退出、在空闲链表、或初始化中);不执行用户代码;可能有或没有栈;由处理其回收/复用的 M 暂时拥有_Genqueue_unused_Gcopystack//正在进行栈移动/拷贝;不执行用户代码;不在运行队列上;栈由发起拷贝的一方拥有_Gpreempted//因 suspendG 抢占而自停,类似 _Gwaiting,但尚无人负责将其 ready;需要某个 suspendG 将状态 CAS 为 _Gwaiting 并负责唤醒//GC 扫描期叠加位,用于标注 goroutine 栈扫描/自扫描期间的状态,除了_Gscanrunning_Gscan = 0x1000_Gscanrunnable = _Gscan + _Grunnable_Gscanrunning = _Gscan + _Grunning_Gscansyscall = _Gscan + _Gsyscall_Gscanwaiting = _Gscan + _Gwaiting_Gscanpreempted = _Gscan + _Gpreempted
)
1.1.2 P的状态常量
const (// P status_Pidle = iota // 空闲:未用于运行用户代码或调度器,通常在空闲队列_Prunning // 运行中:被某个 M 持有,用于运行用户代码或调度器_Psyscall // 系统调用关联:不运行用户代码,与系统调用中的 M 有亲和性,可被其他 M 窃取_Pgcstop // GC 停止:STW 期间暂停,由发起 STW 的 M 持有_Pdead // 已死亡:GOMAXPROCS 变小后不再使用,资源基本被剥离
)
1.2 重要数据结构
1.2.1 gobuf的结构体
// 源码位置:go/src/runtime/runtime2.go
type gobuf struct {// sp, pc, g的偏移量是已知的(硬编码在)libmach中//// ctxt在GC方面是不寻常的:它可能是堆分配的funcval,// 所以GC需要跟踪它,但它需要从汇编中设置和清除,// 在那里很难有写屏障。然而,ctxt实际上是一个保存的活动寄存器,// 我们只在真实寄存器和gobuf之间交换它。// 因此,我们在栈扫描期间将其视为根,这意味着保存和恢复它的汇编不需要写屏障// 它仍然被类型化为指针,以便Go的任何其他写入都获得写屏障sp uintptr // 栈指针pc uintptr // 程序计数器g guintptr // goroutine指针(绕过写屏障)ctxt unsafe.Pointer // 上下文指针,通常指向funcvallr uintptr // 链接寄存器(用于某些架构)bp uintptr // 基指针(用于启用帧指针的架构)//ret uintptr // 1.24.4被删除 返回值 现在直接通过寄存器或 goroutine 的栈传递返回值
}
主要作用:
gobuf是运行时“协程上下文快照”的结构;调度、栈切换、cgo 边界、栈扩容等一切需要“暂停-继续”的地方,都会写/读它;真正的保存和跳转由 gosave/gogo 这类汇编原语完成。
1.2.2 G的结构体
type g struct {// Stack parameters.// stack 描述实际栈内存区间: [stack.lo, stack.hi)。// stackguard0 是 Go 栈增长前序中比较的栈指针。// 通常为 stack.lo+StackGuard,但可设为 StackPreempt 以触发抢占。// stackguard1 是 //go:systemstack 栈增长前序中比较的栈指针。// 在 g0 和 gsignal 栈上为 stack.lo+StackGuard;// 在其他 goroutine 栈上为 ~0,以触发 morestackc 并导致崩溃。stack stack // 偏移量由 runtime/cgo 已知stackguard0 uintptr // 偏移量由 liblink 已知stackguard1 uintptr // 偏移量由 liblink 已知_panic *_panic // 最内层 panic,偏移量由 liblink 已知_defer *_defer // 最内层 deferm *m // 当前绑定的 M,偏移量由 arm liblink 已知sched gobuf // 调度上下文(保存寄存器、PC、SP 等)// 系统调用相关上下文(用于 GC 时保留)syscallsp uintptr // 若 status==Gsyscall,此处存 sched.spsyscallpc uintptr // 若 status==Gsyscall,此处存 sched.pcsyscallbp uintptr // 若 status==Gsyscall,此处存 sched.bp 用于回溯stktopsp uintptr // 回溯时期望的栈顶 SP// param 是通用指针参数字段,用于在特定场景传递值:// 1. channel 操作唤醒阻塞 goroutine 时,指向该次阻塞的 sudog;// 2. gcAssistAlloc1 通知调用者完成 GC 周期(因栈可能已搬迁);// 3. debugCallWrap 传参给新 goroutine(运行时禁止闭包分配);// 4. panic 恢复返回到某帧时,指向 savedOpenDeferState。param unsafe.Pointeratomicstatus atomic.Uint32 // 原子状态stackLock uint32 // sigprof/scang 锁;TODO: 合并到 atomicstatusgoid uint64 // goroutine 唯一 IDschedlink guintptr // 全局可运行队列链接waitsince int64 // 估计阻塞开始时间waitreason waitReason // 若 status==Gwaiting,记录原因// 抢占控制preempt bool // 抢占信号(等价于 stackguard0 = stackpreempt)preemptStop bool // 抢占时转入 _Gpreempted,否则仅去调度preemptShrink bool // 在同步安全点收缩栈// asyncSafePoint 表示 g 在异步安全点停下,此时栈上可能无精确指针信息asyncSafePoint bool// paniconfault 在意外故障地址时 panic 而非直接 crashpaniconfault bool// gcscandone 表示栈扫描已完成,受 status 的 _Gscan 位保护gcscandone bool// throwsplit 禁止在此 g 上进行栈分裂throwsplit bool// activeStackChans 表示未加锁 channel 引用此栈,收缩时需加锁保护activeStackChans bool// parkingOnChan 表示即将 park 在 chansend/chanrecv 上,标记栈收缩不安全点parkingOnChan atomic.Bool// inMarkAssist 表示是否在执行 mark assist(执行跟踪使用)inMarkAssist boolcoroexit bool // coroswitch_m 的参数// raceignore 忽略竞态检测事件raceignore int8 // 忽略竞态检测事件nocgocallback bool // 禁止从 C 回调到 Gotracking bool // 是否跟踪此 G 的调度延迟统计trackingSeq uint8 // 决定是否跟踪的序列号trackingStamp int64 // 开始跟踪的时间戳runnableTime int64 // 可运行时间累计(运行时清零),仅在跟踪时使用lockedm muintptr // LockThread 时锁定的 MfipsIndicator uint8 // FIPS 模式指示syncSafePoint bool // 是否停在同步安全点runningCleanups atomic.Bool // 是否正在运行清理函数sig uint32 // 信号编号writebuf byte // 信号处理写缓冲区sigcode0 uintptr // 信号处理相关寄存器sigcode1 uintptrsigpc uintptrparentGoid uint64 // 创建此 goroutine 的父 goidgopc uintptr // 创建此 goroutine 的 go 语句的 PCancestors *ancestorInfo // debug.tracebackancestors 模式下的祖先链startpc uintptr // goroutine 函数入口 PCracectx uintptr // 竞态检测上下文// waiting 指向当前 g 等待的 sudog 链表(elem 有效)waiting *sudogcgoCtxtuintptr // cgo 回溯上下文labels unsafe.Pointer // 性能分析器标签timer *timer // time.Sleep 缓存的定时器sleepWhen int64 // 睡眠到期时间selectDone atomic.Uint32 // 是否参与 select 及是否赢得唤醒竞赛// goroutineProfiled 表示当前 goroutine 栈在 profile 中的状态goroutineProfiled goroutineProfileStateHoldercoroarg *coro // 协程切换参数bubble *synctestBubble // 同步测试气泡// Per-G 追踪状态trace gTraceState// GC 相关状态// gcAssistBytes 为 GC 辅助信用额度(字节数):// >0 表示有可用额度;// <0 表示需完成扫描工作。// 通过 assist ratio 转换为扫描工作债务。gcAssistBytes int64// valgrindStackID 在 valgrind build tag 下用于跟踪栈内存,否则未使用valgrindStackID uintptr
}
G的状态转换图
1.2.3 M的结构体
type m struct {g0 *g// g0: 持有调度堆栈的goroutinemorebuf gobuf// morebuf: 传递给morestack的gobuf参数divmod uint32// divmod: ARM平台的除法/模运算分母(已知liblink,详见cmd/internal/obj/arm/obj5.go)// 调试器未知的字段procid uint64 // 供调试器使用,但偏移量未硬编码gsignal *g // 信号处理专用的goroutinegoSigStack gsignalStack // Go分配的信号处理堆栈sigmask sigset // 保存的信号掩码存储tls [tlsSlots]uintptr // 线程局部存储(用于x86外部寄存器)mstartfn func() // M启动函数curg *g // 当前运行的goroutinecaughtsig guintptr // 在致命信号期间运行的goroutinep puintptr // 附加的P(用于执行Go代码,未执行Go代码时为nil)nextp puintptr // 下一个Poldp puintptr // 执行系统调用前附加的Pid int64 // M的唯一IDmallocing int32 // 是否在分配内存throwing throwType // 当前抛出类型preemptoff string // 若非空,强制保持curg在此M上运行locks int32 // 保持锁定的次数dying int32 // 死亡状态标志profilehz int32 // 性能分析频率spinning bool // M处于空闲状态并主动寻找工作blocked bool // M被note阻塞newSigstack bool // minit在C线程中调用了sigaltstackprintlock int8 // 打印锁incgo bool // 是否在执行cgo调用isextra bool // 是否为备用MisExtraInC bool // 是否为在C代码中运行的备用MisExtraInSig bool // 是否为在信号处理中运行的备用MfreeWait atomic.Uint32 // 是否可以安全释放g0并删除M(freeMRef/freeMStack/freeMWait之一)needextram bool // 是否需要备用Mg0StackAccurate bool // g0堆栈是否具有准确边界traceback uint8 // 回溯类型allpSnapshot []*p // 附加P时的allp快照(findRunnable释放P后使用,否则为nil)ncgocall uint64 // 总cgo调用次数ncgo int32 // 当前进行的cgo调用次数cgoCallersUse atomic.Uint32 // 若非零,cgoCallers临时可用cgoCallers *cgoCallers // cgo调用崩溃时的回溯信息park note // 用于阻塞M的notealllink *m // 在allm链表中的链接schedlink muintptr // 调度链表链接lockedg guintptr // 锁定的goroutinecreatestack [32]uintptr // 创建此线程的堆栈(用于StackRecord.Stack0,必须对齐)lockedExt uint32 // 外部LockOSThread状态追踪lockedInt uint32 // 内部lockOSThread状态追踪mWaitList mWaitList // 运行时锁等待者列表mLockProfile mLockProfile // 与runtime.lock争用相关的字段profStackuintptr // 用于内存/阻塞/互斥锁堆栈追踪// wait*字段用于从gopark传递参数到park_m(因为低级NOSPLIT函数没有堆栈)waitunlockf func(*g, unsafe.Pointer) bool // 解锁函数waitlock unsafe.Pointer // 等待的锁waitTraceSkip int // 跟踪跳过次数waitTraceBlockReason traceBlockReason // 跟踪阻塞原因syscalltick uint32 // 系统调用计数freelink *m // 在sched.freem链表中的链接trace mTraceState // 跟踪状态// 这些字段太大,不能放在低级NOSPLIT函数的堆栈中libcall libcall // 系统调用参数libcallpc uintptr // 用于CPU性能分析libcallsp uintptr // 调用堆栈指针libcallg guintptr // 当前goroutinewinsyscall winlibcall // Windows平台的系统调用参数vdsoSP uintptr // VDSO调用中的堆栈指针(未调用时为0)vdsoPC uintptr // VDSO调用中的程序计数器// preemptGen: 记录已完成的抢占信号次数(用于检测抢占失败)preemptGen atomic.Uint32// 该M是否有待处理的抢占信号signalPending atomic.Uint32// pcvalue查找缓存pcvalueCache pcvalueCachedlogPerM // 每M的日志记录mOS // 操作系统相关字段chacha8 chacha8rand.State // ChaCha8随机数生成器状态cheaprand uint64 // 快速随机数生成器// 该M持有的最多10把锁(由锁排序代码维护)locksHeldLen int // 持有锁数量locksHeld [10]heldLockInfo // 持有锁信息数组
}
M的内存大小
const mRedZoneSize = (16 << 3) * asanenabledBit // redZoneSize(2048)type mPadded struct {m// Size the runtime.m structure so it fits in the 2048-byte size class, and// not in the next-smallest (1792-byte) size class. That leaves the 11 low// bits of muintptr values available for flags, as required by// lock_spinbit.go._ [(1 - goarch.IsWasm) * (2048 - mallocHeaderSize - mRedZoneSize - unsafe.Sizeof(m{}))]byte
}
mRedZoneSize是在启用Ascan时,作为栈溢出检测的区域,大小为128字节。也用于栈扩展部分,当stackstackguard0进入red zone,会触发gorwStack扩展栈。
mPadded
的设计目的是 通过填充字段 确保 m
结构体大小为 2048 字节,从而在 muintptr
中 保留低 11 位用于标志位。该设计在非 Wasm 平台生效,Wasm 平台因内存模型差异跳过填充。核心优化点在于 标志位复用 和 内存对齐,避免额外内存分配,提升并发性能。
1.2.3 P的结构体
type p struct {id int32 // 进程IDstatus uint32 // 状态(如 pidle/prunning/...)link puintptr // 链表指针schedtick uint32 // 每次调度调用时递增的计数器syscalltick uint32 // 每次系统调用时递增的计数器sysmontick sysmontick // sysmon 上次观察到的计数器m muintptr // 关联的 M(空闲时为 nil)mcache *mcache // 本地 M 的缓存pcache pageCache // 页缓存raceprocctx uintptr // 竞态检测上下文deferpool []*_defer // 可用 defer 结构体池(见 panic.go)deferpoolbuf [32]*_defer // defer 结构体缓冲区// 缓存的 goroutine ID,减少对 runtime·sched.goidgen 的访问goidcache uint64 // goroutine ID 缓存起始值goidcacheend uint64 // 缓存的 goroutine ID 终止值// 可运行的 goroutine 队列(无锁访问)runqhead uint32 // 队列头部索引runqtail uint32 // 队列尾部索引runq [256]guintptr // 队列数组// runnext 存储当前 G 准备运行的下一个 G(若时间片未用尽)。// 它继承当前时间片的剩余时间。若一组 goroutine 处于通信和等待模式中,// 此字段可将该组作为一个单元调度,避免将 goroutine 添加到队列尾部导致的调度延迟。// 注意:其他 P 可以原子地将此字段置为零,但只有当前 P 可以原子地设置为有效 G。runnext guintptr// 可用的 G(状态 == Gdead)gFree gList // 可回收的 G 列表sudogcache []*sudog // sudog 缓存sudogbuf [128]*sudog // sudog 缓冲区// 从堆中缓存的 mspan 对象mspancache struct {// 显式长度字段,避免在分配路径中使用写屏障时的复杂性len int // 当前缓存长度buf [128]*mspan // 缓存数组}// 缓存的单个 pinner 对象,减少重复创建 pinner 的分配开销pinnerCache *pinnertrace pTraceState // 跟踪状态palloc persistentAlloc // 每个 P 的持久化分配器// 每个 P 的 GC 状态gcAssistTime int64 // GC 辅助分配所花费的时间(纳秒)gcFractionalMarkTime int64 // 分数标记工作者所花费的时间(纳秒,原子更新)// limiterEvent 跟踪 GC CPU 限制器的事件limiterEvent limiterEvent// gcMarkWorkerMode 指示下一个标记工作者应运行的模式// 用于与通过 gcController.findRunnableGCWorker 选择的工作者 goroutine 通信// 调度其他 goroutine 时,此字段必须设置为 gcMarkWorkerNotWorkergcMarkWorkerMode gcMarkWorkerMode// gcMarkWorkerStartTime 是最近标记工作者的启动时间(纳秒)gcMarkWorkerStartTime int64// gcw 是此 P 的 GC 工作缓冲区缓存// 缓冲区由写屏障填充,由突变器辅助消耗,在特定 GC 状态转换时释放gcw gcWork// wbBuf 是此 P 的 GC 写屏障缓冲区// TODO: 考虑将其缓存在运行的 G 中wbBuf wbBufrunSafePointFn uint32 // 若为 1,调度器在下一个安全点运行 sched.safePointFn// statsSeq 是指示此 P 是否正在写入统计信息的计数器// 偶数表示未写入,奇数表示正在写入statsSeq atomic.Uint32// 定时器堆timers timers// 清理块cleanups *cleanupBlockcleanupsQueued uint64 // 此 P 队列的清理块数量(单调递增)// maxStackScanDelta 累积活动 goroutine 的栈空间占用(即可能需要扫描的栈大小)// 当达到 maxStackScanSlack 或 -maxStackScanSlack 时,刷新到 gcController.maxStackScanmaxStackScanDelta int64// GC 时间统计// 与 maxStackScan 不同,该字段累积 GC 时实际观察到的栈使用量(hi - sp)// 而非瞬时的总栈大小(hi - lo)scannedStackSize uint64 // 此 P 扫描的 goroutine 栈大小scannedStacks uint64 // 此 P 扫描的 goroutine 数量// preempt 标记此 P 需尽快进入调度器(无论当前 G 在运行什么)preempt bool// gcStopTime 是此 P 最近进入 _Pgcstop 的时间戳(纳秒)gcStopTime int64
}
1.2.4 schedt结构体
type schedt struct {// goidgen 是全局唯一的 goroutine ID 生成器// lastpoll 记录上次网络轮询的时间(若当前正在轮询则为 0)// pollUntil 记录当前轮询的睡眠截止时间// pollingNet 表示是否有 P 正在执行非阻塞的网络轮询goidgen atomic.Uint64lastpoll atomic.Int64 // 上次网络轮询时间pollUntil atomic.Int64 // 当前轮询的睡眠截止时间pollingNet atomic.Int32 // 1 表示有 P 正在执行非阻塞轮询// lock 是全局调度器的互斥锁lock mutex// 增加 nmidle、nmidlelocked、nmsys 或 nmfreed 时,必须调用 checkdead()// midle 是等待工作的空闲 M 链表// nmidle 是空闲 M 的数量// nmidlelocked 是被锁定的空闲 M 数量// mnext 是已创建的 M 总数和下一个 M 的 ID// maxmcount 是允许的最大 M 数量(超过则终止)// nmsys 是系统 M 的数量(不计入死锁检测)// nmfreed 是累计释放的 M 数量midle muintptr // 空闲 M 链表nmidle int32 // 空闲 M 数量nmidlelocked int32 // 被锁定的空闲 M 数量mnext int64 // 已创建的 M 数量和下一个 M IDmaxmcount int32 // 最大允许的 M 数量nmsys int32 // 系统 M 数量nmfreed int64 // 累计释放的 M 数量// ngsys 是系统 goroutine 的数量ngsys atomic.Int32// pidle 是空闲 P 链表// npidle 是空闲 P 数量(原子更新)// nmspinning 是自旋的 M 数量(参考 proc.go 中的“Worker thread parking/unparking”注释)// needspinning 是是否需要自旋的标志(参考 proc.go 中的“Delicate dance”注释,布尔值,修改时需持有 sched.lock)pidle puintptr // 空闲 P 链表npidle atomic.Int32nmspinning atomic.Int32needspinning atomic.Uint32// 全局可运行队列runq gQueue// disable 控制调度器的禁用(通过 schedEnableUser 控制)// disable.user 表示是否禁用用户 goroutine 的调度// disable.runnable 是待运行的 G 队列disable struct {user boolrunnable gQueue // 待运行的 G 队列}// gFree 是全局死亡 G 的缓存gFree struct {lock mutexstack gList // 带栈的 G 列表noStack gList // 无栈的 G 列表}// sudoglock 保护 sudogcache 的互斥锁// sudogcache 是全局 sudog 缓存sudoglock mutexsudogcache *sudog// deferlock 保护 deferpool 的互斥锁// deferpool 是全局 defer 结构体缓存deferlock mutexdeferpool *_defer// freem 是 m.exited 被设置后等待释放的 M 链表(通过 m.freelink 连接)freem *m// gcwaiting 表示 GC 是否在等待运行// stopwait 和 stopnote 用于 stop-the-world 等待// sysmonwait 表示 sysmon 是否在等待// sysmonnote 是 sysmon 的等待信号gcwaiting atomic.Bool // GC 等待运行标志stopwait int32stopnote notesysmonwait atomic.Boolsysmonnote note// safePointFn 是在下一个 GC 安全点需要调用的函数(若 p.runSafePointFn 被设置)// safePointWait 是等待计数// safePointNote 是安全点的等待信号safePointFn func(*p)safePointWait int32safePointNote note// profilehz 是 CPU 采样率profilehz int32// procresizetime 是上次调整 gomaxprocs 的时间(纳秒)// totaltime 是从 procresizetime 开始的累计运行时间procresizetime int64 // 上次调整 gomaxprocs 的时间totaltime int64 // 累计运行时间// customGOMAXPROCS 表示 GOMAXPROCS 是否被手动设置(环境变量或 runtime.GOMAXPROCS)customGOMAXPROCS bool// sysmonlock 是保护 sysmon 与运行时交互的互斥锁// 持有该锁可阻断 sysmon 对运行时的操作sysmonlock mutex// timeToRun 是调度延迟分布(定义为 G 在 _Grunnable 状态到 _Grunning 状态的总时间)timeToRun timeHistogram// idleTime 是所有 P 的空闲时间总和(每次 GC 周期重置)idleTime atomic.Int64// totalMutexWaitTime 是 goroutine 在 _Gwaiting 状态等待 runtime 内部锁的总时间totalMutexWaitTime atomic.Int64// stwStoppingTimeGC/Other 是 stop-the-world 停止延迟分布(定义为 stopTheWorldWithSema 到所有 P 停止的时间)// stwStoppingTimeGC 覆盖所有 GC 相关的 STW,stwStoppingTimeOther 覆盖其他 STWstwStoppingTimeGC timeHistogramstwStoppingTimeOther timeHistogram// stwTotalTimeGC/Other 是 stop-the-world 总延迟分布(定义为 stopTheWorldWithSema 到 startTheWorldWithSema 的总时间)// stwTotalTimeGC 覆盖所有 GC 相关的 STW,stwTotalTimeOther 覆盖其他 STWstwTotalTimeGC timeHistogramstwTotalTimeOther timeHistogram// totalRuntimeLockWaitTime(加上每个 M 的 lockWaitTime)是 goroutine 在 _Grunnable 状态且持有 M 但等待 runtime 内部锁的总时间// 该字段存储已退出 M 的累计时间totalRuntimeLockWaitTime atomic.Int64
}
剩余结构体作用
type libcall struct {fn uintptrn uintptr // 参数个数args uintptr // 参数列表r1 uintptr // 返回值1r2 uintptr // 返回值2err uintptr // 错误号
}// Stack 描述了 Go 运行时的执行栈。
// 栈的边界恰好是 [lo, hi),
// 两端没有任何隐式的数据结构。
type stack struct {lo uintptrhi uintptr
}// heldLockInfo 提供已持有的锁及该锁等级的信息
type heldLockInfo struct {lockAddr uintptrrank lockRank
}
其中libcall在汇编/系统层面,runtime 会构造一个 libcall,把要调用的函数地址、参数列表打包到它的各字段里,然后由通用的调用入口(如 asm/syscall 实现)读取这些字段并真正发起调用,返回值和 errno 也写回到这里。
每个 G 对象里有一个 stack 字段,用 lo、hi 精确标识它的栈内存区域(lo ≤ sp < hi)。GC、栈扩展/收缩、调度等子系统都依赖这两个边界来判断栈是否需要 grow/shrink,以及扫描活跃帧时的地址合法性
Go 运行时为了在调试模式下检测可能的死锁或锁顺序反转,会给每把锁分配一个 rank。每当 G 获取一把锁,就往它的 heldLocks 列表里插入一个 heldLockInfo。释放时再删掉。这样就能在运行时断言“只允许按 rank 升序获取锁”,及时报告不安全的锁顺序。
参考文献
https://juejin.cn/post/7519334402688368667#heading-11
https://zhuanlan.zhihu.com/p/67852800