vault backup: 2026-06-07 22:13:27

This commit is contained in:
2026-06-07 22:13:27 +08:00
parent dcbf185468
commit 60331be228
3 changed files with 1232 additions and 0 deletions
+226
View File
@@ -0,0 +1,226 @@
---
tags: [go, golang, go-principle, gmp-scheduler, test, mcq]
create time: 2026-06-07
title: GMP 调度原理 — 选择题测试
questions: 20
---
# GMP 调度原理 — 选择题测试
共 20 题,涵盖 GMP 概览、数据结构、生命周期和抢占式调度四大模块。每题仅有一个正确答案,答案以折叠块形式位于每题之后,点击展开查看。
---
## 一、GMP 架构概览(第 1~5 题)
**1. Go 为什么没有直接使用操作系统内核线程来承载 goroutine?**
A. 操作系统不支持多线程
B. 内核线程上下文切换成本高,goroutine 是用户态轻量级执行流,切换仅需修改栈指针,开销极小
C. Go 语言规范禁止使用系统线程
D. 内核线程无法执行并行计算
> [!info]- 答案
> **B** — 内核线程上下文切换需陷入内核态(保存寄存器、切换页表等),开销大;goroutine 在用户态运行,切换仅需改栈指针,成本极低。(§1.1)
**2. GMP 模型中,P(Processor)的角色最接近以下哪个类比?**
A. 工作任务
B. 工人
C. 车间主管
D. 工厂仓库
> [!info]- 答案
> **C** — G = 工作任务、M = 工人、P = 车间主管。P 管理本地队列,协调 M 与 G 的关系。(§1.3 表格)
**3. LRQ(Local Run Queue)与 GRQ(Global Run Queue)的主要区别是什么?**
A. LRQ 需要加锁,GRQ 无锁
B. LRQ 是每个 P 私有的无锁队列,GRQ 是所有 P 共享的全局队列需加锁
C. LRQ 容量无限,GRQ 最多 256 个 G
D. LRQ 用于存放阻塞的 G,GRQ 用于存放运行的 G
> [!info]- 答案
> **B** — LRQ per-P 私有,无锁 CAS 操作,容量 256;GRQ 全局共享,需 `sched.lock`,容量理论上无限。(§1.3 两种队列)
**4. 当 M 上的 P 本地队列和全局队列都为空时,findRunnable 接下来会做什么?**
A. 直接让 M 进入永久休眠
B. 向操作系统申请更多 CPU 核心
C. 通过 netpoll 检查 IO 就绪事件,若仍无则进行 work-stealing
D. 强制杀死所有当前正在运行的 G
> [!info]- 答案
> **C** — findRunnable 优先级:LRQ → GRQ → netpoll IO 就绪 → work-stealing → 失败才休眠。(§1.3 / §3.4 流程图)
**5. Go 防止 GRQ 中的 G 长期饥饿的策略是什么?**
A. 每 10ms 强制清空 LRQ
B. 每 61 次调度循环强制检查一次 GRQ
C. 每个 P 固定分配 50% 时间处理 GRQ
D. GRQ 中的 G 拥有最高优先级
> [!info]- 答案
> **B** — 每 61 次调度循环(`schedtick % 61 == 0`)强制检查 GRQ,保证公平性防饥饿。(§1.3 note / §2.3 表格)
---
## 二、数据结构(第 6~10 题)
**6. G 结构体中 `stackguard0` 字段的用途是什么?**
A. 记录 goroutine 的创建时间
B. 作为栈保护区边界值,用于触发栈扩容或传递抢占标记
C. 存储 goroutine 的优先级
D. 指向下一个 goroutine 的指针
> [!info]- 答案
> **B** — `stackguard0` 用于函数调用前比较,等于 `stackPreempt` 表示被抢占,接近 `stack.lo` 触发栈扩容。(§2.1 G 结构)
**7. M(Machine)结构体中 `g0` 的特殊作用是什么?**
A. 它是第一个被创建的普通用户协程
B. 它是调度协程,M 用它来执行调度逻辑(如 schedule、newproc)
C. 它负责处理所有的网络 IO
D. 它是垃圾回收专用的协程
> [!info]- 答案
> **B** — `g0` 是调度协程,每个 M 独有。M 执行 `g0` 时是调度者,执行 `curg` 时是执行者。(§2.2 M 结构)
**8. P 结构体中 `runnext` 字段的作用是什么?**
A. 记录 P 的编号
B. 作为 VIP 位置,新创建或高优先级的 G 优先放入,下次调度直接跳过队列执行
C. 指向下一个可用的空闲 P
D. 记录 P 已经运行了多久
> [!info]- 答案
> **B** — `runnext` 是 VIP 位,新创建的高优先级 G 优先放此处,跳过队列开销直接执行。(§2.3 P 结构)
**9. `schedt`(全局调度器)中的 `midle` 和 `pidle` 两个空闲队列的设计意图是什么?**
A. 用于调试时统计空闲资源数量
B. 实现资源的休眠与复用——不忙时释放回池中,有新任务时快速唤醒
C. 为系统崩溃时恢复资源提供备份
D. 用于将资源转移给其他进程的运行时
> [!info]- 答案
> **B** — `midle` 和 `pidle` 实现资源休眠与复用——不忙释放回池,有新任务快速唤醒。(§2.4 schedt note)
**10. G 的状态 `_Gwaiting` 有什么特点?**
A. G 被放置在 LRQ 中等待再次调度
B. G 不入任何就绪队列,由上层调用者(channel/mutex 等)自行保管
C. G 被立即终止并回收
D. G 被提升为最高优先级
> [!info]- 答案
> **B** — `_Gwaiting` 的 G 不入任何就绪队列,由上层(channel/mutex/Timer 等)保管,等待上层唤醒。(§4.4 gopark 流程)
---
## 三、Goroutine 生命周期(第 11~16 题)
**11. `go func(){...}` 语句最终被编译器转换为何种 runtime 函数调用?**
A. `runtime.new()`
B. `runtime.schedule()`
C. `runtime.newproc()`
D. `runtime.gopark()`
> [!info]- 答案
> **C** — `go func(){...}` 被编译器转换为 `runtime.newproc(fn)` 调用。(§3.2 创建流程)
**12. 创建新 G 时,为什么需要通过 `systemstack` 从用户 G 栈切换到 `g0` 栈?**
A. 因为用户栈空间不够
B. 因为创建 G 是调度层面的工作,必须交由调度栈 `g0` 处理
C. 因为 `g0` 栈有更高的优先级
D. 因为用户栈不支持函数调用
> [!info]- 答案
> **B** — 创建 G 涉及分配 struct、初始化入口地址等调度层面工作,必须在 `g0` 调度栈上执行。(§3.2 核心步骤①)
**13. `findRunnable()` 查找可运行 G 的正确优先级顺序是什么?**
A. GRQ → LRQ → netpoll → work-stealing
B. LRQ → GRQ → netpoll → work-stealing
C. netpoll → LRQ → GRQ → work-stealing
D. work-stealing → netpoll → LRQ → GRQ
> [!info]- 答案
> **B** — 严格顺序:LRQ 无锁 CAS → GRQ 加锁 → netpoll IO → work-stealing。源码路径 `runtime/proc.go`。(§3.4 流程图)
**14. Work-Stealing 机制中,P 从其他繁忙 P 那里窃取的比例是多少?**
A. 全部 G
B. 一半 G
C. 最后一个 G
D. 随机数量 G
> [!info]- 答案
> **B** — `runqsteal` 窃取目标 P LRQ 中的一半而非全部,减少反复争夺。(§3.7 Work-Stealing)
**15. Goroutine 主动让出的三种方式中,`Gosched` 和 `goexit` 的关键区别是什么?**
A. `Gosched` 将 G 放回 gfree 链表,`goexit` 将 G 放回 GRQ
B. 两者没有区别,完全等价
C. `Gosched` 将 G 放回 GRQ 等待再次调度,`goexit` 将 G 放回 gfree 彻底回收
D. `Gosched` 直接将 G 发送给另一个 M,`goexit` 将 G 放入 LRQ
> [!info]- 答案
> **C** — `Gosched` → `_Grunnable` → globrunqput(GRQ),等待再调度;`goexit` → `_Gdead` → gfput(gfree),彻底回收复用。(§4.2 vs §4.3 note)
**16. `goready()` 唤醒一个处于 `_Gwaiting` 状态的 G 时,以下操作的正确顺序是?**
A. 改为 `_Grunning` → runqput → wakep()
B. 改为 `_Grunnable` → runqput(LRQ 或 GRQ) → wakep()
C. runqput → 改为 `_Grunning` → wakep()
D. wakep() → 改为 `_Grunnable` → runqput
> [!info]- 答案
> **B** — `ready()` 三步:casgstatus(`_Gwaiting`→`_Grunnable`) → runqput(入 LRQ 或 GRQ) → wakep() 唤醒空闲 P。(§4.4 goready 源码)
---
## 四、抢占式调度(第 17~20 题)
**17. sysmon 线程三次巡检的核心职责分别是?**
A. netpoll(IO 轮询)、retake(抢占检查)、GC 触发
B. schedule(调度)、netpoll(IO 轮询)、memory alloc(内存分配)
C. retake(抢占检查)、lock(加锁)、unlock(解锁)、stack resize(栈扩容)
D. GC 触发、wakeup(唤醒 G)、stopm(停止 M)
> [!info]- 答案
> **A** — sysmon 三板斧:① netpoll 取 epoll 就绪事件 → injectglist;② retake 抢占超时/syall P;③ GC 触发检查。(§5.1 三次巡检表格)
**18. 当一个 G 发起系统调用时,`reentersyscall` 做了什么?(单选)**
A. 将该 G 直接放入 GRQ 并停止 M
B. 将 G 状态改为 `_Gsyscall`,解除 P 与 M 的绑定,并将 P 状态设为 `_Psyscall`
C. 复制一个新的 P 来继续调度
D. 暂停整个程序直到 syscall 返回
> [!info]- 答案
> **B** — `reentersyscall`: G→`_Gsyscall`, pp.m=0 (解绑 P→M), _g_.m.p=0, atomic.Store(&pp.status, _Psyscall)。(§5.2 源码)
**19. Go ≥ 1.14 引入的非协作式抢占是如何工作的?**
A. 通过设置 `stackPreempt` 标记在下次函数调用时生效
B. 通过 POSIX 信号 (`sigPreempt`) 发送到目标线程,在安全中断点注入 `asyncPreempt` 强行接管执行流
C. 通过 kill 信号终止该 G 所在的 OS 线程
D. 通过降低该 G 所属 P 的优先级来间接实现
> [!info]- 答案
> **B** — Go ≥ 1.14: preemptone 发送 `sigPreempt` 信号 → sighandler → doSigPreempt → pushCall(asyncPreempt) 劫持 PC。(§5.3.2 源码 + 流程图)
**20. 为什么 Go 在引入了非协作式抢占后仍然保留协作式抢占作为兜底方案?**
A. 因为协作式抢占的性能更好
B. 因为信号机制存在平台限制(如 Windows 不支持),协作式兜底;且对有 IO/channel 的 G 协作式已足够及时
C. 因为开发者可以手动选择使用哪种抢占方式
D. 因为非协作式抢占只在 Linux 上有效
> [!info]- 答案
> **B** — 信号机制有平台限制(Windows 不支持),协作式兜底;对有 IO/channel 的 G 协作式已足够及时。(§5.4 note)