Files
cs-note/hzh/TEST/GMP调度原理.md
T

227 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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)