线上接口突然变慢时,我们通常会先看 CPU。
但有一种问题非常反直觉:
- CPU 使用率只有 60%;
- 服务吞吐量没有明显下降;
- 平均响应时间基本正常;
- P99 延迟却从 200 毫秒增长到了 2 秒;
- 扩容实例后,问题只能暂时缓解。
这类问题不一定是 CPU 算力不足,也可能是关键线程没有及时获得 CPU。
在同一台服务器上,可能同时运行着请求处理、日志压缩、缓存淘汰、垃圾回收、监控采集和批处理任务。Linux 调度器能够看到哪些线程处于可运行状态,却不知道哪些线程正位于用户请求的关键路径,也不知道某个后台任务晚执行几十毫秒几乎没有影响。
传统解决办法通常是调整线程优先级、绑定 CPU、修改 cgroup 参数,或者直接增加服务器。
但当固定参数已经无法表达复杂的业务策略时,过去只有一条更彻底的路:修改 Linux 内核调度器。
这意味着编写内核补丁、重新编译内核、重启服务器、验证稳定性,并承担系统崩溃的风险。
sched_ext 改变了这条路径。
它允许开发者使用 eBPF 编写 CPU 调度策略,通过用户态程序动态加载到 Linux 内核中。更换调度策略不再一定需要修改和重启整个内核,CPU 调度开始从“内核里的固定实现”,变成一种可以快速部署、试验和回退的系统策略。
一、CPU 使用率正常,为什么接口还是会卡
CPU 使用率只能说明 CPU 在一段时间内有多忙,却不能说明某个线程等待了多久。
假设一台服务器拥有 32 个 CPU 核心,运行着两类任务:
| 任务类型 | 特点 | 对延迟的敏感程度 |
|---|---|---|
| 在线请求线程 | 单次运行时间短,数量多 | 非常敏感 |
| 后台计算线程 | 单次占用时间长,可以延后 | 不敏感 |
从总体 CPU 使用率来看,服务器可能还有空闲能力。
但如果在线请求线程被安排到繁忙的运行队列,或者频繁在不同 CPU 之间迁移,它仍然可能经历较长的调度等待时间。
因此,下列现象完全可能同时出现:
| 监控现象 | 真实情况 |
|---|---|
| CPU 使用率没有达到 100% | 某些 CPU 的运行队列已经拥堵 |
| 所有核心负载比较平均 | 线程频繁迁移,缓存命中率下降 |
| 平均延迟正常 | 少量关键请求等待时间异常 |
| 吞吐量变化不大 | P99、P999 长尾明显恶化 |
| 增加线程后性能下降 | 上下文切换和竞争进一步增加 |
通用调度器需要同时兼顾桌面、数据库、编译、容器、虚拟机和普通服务器等大量场景,因此不能默认理解每一种业务的优先级。
对调度器来说,两个处于可运行状态的线程只是两个任务。
但对业务系统来说,其中一个线程可能正在生成广告推荐结果,另一个线程可能只是整理昨天的日志。
这就是通用公平策略与业务关键路径之间的语义差距。
二、sched_ext 到底改变了什么
sched_ext 是 Linux 内核中的可扩展调度类,它允许一组 BPF 程序定义调度行为。
它已经从 Linux 6.12 开始进入上游内核。调度策略可以动态加载和卸载,出现内部错误、可运行任务长时间停滞或者管理员主动触发回退时,系统会终止当前 BPF 调度器,并恢复到内核默认的 fair-class 调度器。(Linux内核文档)
传统调度器优化流程通常是:
flowchart LR
A[发现调度问题] --> B[修改内核代码]
B --> C[重新编译内核]
C --> D[重启服务器]
D --> E[压测验证]
E --> F[逐步发布]
引入 sched_ext 后,流程可以变成:
flowchart LR
A[发现调度问题] --> B[编写 BPF 策略]
B --> C[用户态程序加载]
C --> D[运行时压测]
D --> E{效果是否符合预期}
E -->|是| F[扩大灰度]
E -->|否| G[停止调度器]
G --> H[恢复默认调度器]
变化的重点并不是“把调度器完全搬到用户态”。
真正执行调度决策的 BPF 程序仍然运行在内核上下文中,用户态程序主要负责加载程序、提供配置、输出统计信息,以及在部分实现中参与较复杂的策略计算。
可以将整套系统分成三层:
| 层级 | 主要职责 |
|---|---|
| Linux 调度核心 | 管理线程状态、CPU 和调度类 |
| sched_ext BPF 程序 | 决定线程怎样排队、选择 CPU 和获得时间片 |
| 用户态加载器 | 加载策略、传递配置、输出指标和控制生命周期 |
因此,sched_ext 不是普通应用层插件,而是一个由 Linux 内核提供安全边界的可编程调度框架。
三、一个线程从唤醒到运行经历了什么
在 sched_ext 中,一个线程从被唤醒到真正获得 CPU,通常会经过下面这条路径:
flowchart LR
A[线程被唤醒] --> B[select_cpu 选择 CPU]
B --> C[enqueue 任务入队]
C --> D[本地 全局或自定义 DSQ]
D --> E[dispatch 派发任务]
E --> F[CPU 本地 DSQ]
F --> G[线程开始运行]
1. select_cpu:选择候选 CPU
当线程从睡眠状态变成可运行状态时,调度器可以通过 select_cpu 选择一个候选 CPU。
这个结果主要是优化提示,并不是不可更改的最终决定。
如果选择的 CPU 不在该线程允许使用的 CPU 集合中,内核调度核心会忽略无效结果,并选择其他合法 CPU。合理的 CPU 选择可以减少跨核迁移,同时在目标 CPU 空闲时及时将其唤醒。(Linux内核文档)
策略可以考虑:
- 线程上一次运行在哪个 CPU;
- 目标 CPU 是否空闲;
- 两个 CPU 是否共享同一个末级缓存;
- 当前线程属于哪个业务分组;
- 目标 NUMA 节点是否拥有本地内存;
- 是否需要为关键请求保留部分 CPU。
2. enqueue:决定任务排到哪里
如果任务没有在 select_cpu 阶段直接进入本地队列,内核会调用 enqueue。
调度器可以选择:
- 放入某个 CPU 的本地调度队列;
- 放入全局调度队列;
- 放入自定义调度队列;
- 暂时保存在 BPF 数据结构中,等待后续决策。
这一步决定了任务与哪些线程竞争,也决定了任务会以什么顺序被选中。
3. dispatch:给空闲 CPU 找任务
当 CPU 需要选择下一个任务时,会先检查自己的本地调度队列。
如果本地队列为空,再尝试从全局队列获得任务。如果仍然没有可运行任务,内核才会调用 BPF 调度器的 dispatch 回调,让自定义策略向该 CPU 派发任务。(Linux内核文档)
因此,一个完整的调度策略通常需要回答三个问题:
任务应该去哪一个 CPU?
任务应该进入哪一条队列?
当 CPU 空闲时,应该优先运行哪一个任务?
四、DSQ 是 sched_ext 的核心抽象
sched_ext 使用 DSQ,也就是 Dispatch Queue,连接 BPF 调度策略和 Linux 调度核心。
DSQ 可以按照 FIFO 方式工作,也可以作为优先队列使用。系统默认提供一个全局 DSQ,并为每个 CPU 提供一个本地 DSQ;BPF 调度器还可以创建任意数量的自定义 DSQ。CPU 最终始终从自己的本地 DSQ 取出任务执行。(Linux内核文档)
可以把它理解成高速公路的车道系统。
| DSQ 类型 | 类比 | 适用场景 |
|---|---|---|
| 本地 DSQ | 某个收费口的专用车道 | 直接交给指定 CPU |
| 全局 DSQ | 所有收费口共享的等待区 | 简单、公平的全局兜底 |
| 自定义 DSQ | 按车辆类型划分的车道 | 业务优先级、NUMA、租户隔离 |
| BPF 内部队列 | 调度中心等待区 | 实现更复杂的决策逻辑 |
例如,在线服务可以创建三类队列:
- 关键请求队列;
- 普通请求队列;
- 后台任务队列。
当 CPU 空闲时,调度器优先从关键请求队列取任务;只有关键队列没有任务时,才处理普通请求和后台任务。
这并不只是简单提高线程优先级。
更复杂的调度器还可以动态调整每类任务能够使用的 CPU 数量:
- 高峰期为关键请求增加 CPU;
- 低峰期把空闲 CPU 让给批处理任务;
- 避免后台任务迁移到关键请求使用的缓存域;
- 防止某类任务长期独占全部 CPU;
- 根据服务延迟实时调整时间片。
调度策略由此从一组固定参数,升级为可以表达状态和反馈逻辑的程序。
五、为什么使用 eBPF,而不是直接加载内核模块
CPU 调度器位于操作系统的核心路径。
一旦调度器出现死循环、丢失可运行任务或者破坏内核状态,整台服务器都有可能失去响应。
sched_ext 选择 eBPF,重要原因之一就是利用 BPF 的验证、限制和退出机制,为调度器试验建立安全边界。
| 保护机制 | 作用 | 不能解决的问题 |
|---|---|---|
| BPF Verifier | 加载前检查程序和内存访问 | 不能保证业务策略一定合理 |
| 动态加载和卸载 | 不重启内核即可切换策略 | 切换仍需经过充分测试 |
| 可运行任务停滞检测 | 防止线程被永久遗忘 | 触发前仍可能出现延迟 |
| 自动回退 | 出错后恢复默认调度器 | 无法恢复已经超时的请求 |
| 调试转储 | 保存失败时的调度状态 | 仍需要完善的分析工具 |
Linux 内核文档明确说明,当检测到内部错误、可运行任务停滞,或者触发 SysRq-S 时,当前 BPF 调度器会被终止,任务会回到默认的 fair-class 调度器。BPF 调度器发生错误时,系统还会生成调试信息;SysRq-D 可以主动触发调试转储,而不立即终止调度器。(Linux内核文档)
此外,BPF Verifier 会在程序加载阶段检查大量不安全行为,降低调度器破坏内核内存的风险。官方设计说明也强调,sched_ext 会通过内核侧机制防止错误调度器无限期饿死任务。(GitHub)
不过,“不会轻易把内核弄崩”并不等于“调度策略一定安全”。
一个逻辑错误的调度器仍然可能:
- 让某类任务等待过久;
- 造成大量无效 CPU 迁移;
- 降低缓存命中率;
- 破坏租户之间的公平性;
- 让吞吐量和长尾延迟同时恶化;
- 在看门狗回退前制造业务故障。
因此,sched_ext 降低的是内核试验门槛,而不是取消性能工程和发布治理。
六、Meta 如何用 sched_ext 优化广告服务
通用调度器的局限,在大型在线服务中表现得尤其明显。
2026 年 7 月,Meta 披露了其广告服务使用 sched_ext 的生产实践。
Meta 的广告工作负载中既有位于延迟关键路径的线程,也有对延迟不太敏感的工作。其自定义策略将 CPU 软划分为两个池:
- 一个处理延迟敏感任务;
- 一个处理非关键任务。
两个 CPU 池的规模会根据负载动态调整。策略还会尽量让相关线程持续运行在相近的 CPU 上,以提高末级缓存局部性,减少访问远端内存的成本。(Engineering at Meta)
Meta 披露,其初始上线在最大的广告服务服务器类型上取得了:
- 广告检索阶段 P99 延迟降低 28%;
- 加权广告排名数量提升 1.1%;
- 整个机群节省约 3.28 兆瓦功耗。
随后两次仅修改用户态调度策略的更新,又带来了额外的 P99 延迟下降和关键路径超时减少。Meta 特别强调,后续调度策略可以按天级节奏迭代,而不再完全依赖内核发布周期。(Engineering at Meta)
这些数字是 Meta 在特定硬件、内核版本和广告工作负载上披露的结果,不能直接推导出其他系统也能获得相同收益。
但它证明了一件重要的事情:
当业务已经达到足够规模时,调度器不再只是操作系统内部实现,也可能直接影响业务指标和基础设施成本。
另一个案例来自 Meta 的 GPU 训练机群。
在多 CPU 插槽和大量 GPU 共同工作的训练节点中,数据加载、预处理、检查点保存和通信线程都运行在 CPU 上。微小的 CPU 调度延迟可能导致 GPU 等待数据。Meta 在 Linux Plumbers Conference 2025 上披露,相关策略已经部署到拥有数万块 GPU 的机群中,并使部分模型的 GPU 计算单元利用率提高约 9%。(Indico)
这说明所谓“GPU 利用率不足”,根因不一定在 GPU,也可能是负责供给数据和触发任务的 CPU 线程没有及时运行。
七、sched_ext 不能替代 cgroup
在容器和 Kubernetes 环境中,最容易产生的误解是:
既然 sched_ext 可以控制 CPU 调度,那是不是不再需要 cgroup?
答案是否定的。
两者解决的问题不同。
| 机制 | 主要职责 |
|---|---|
| cgroup | 定义进程组的资源边界、权重和统计范围 |
| CPU affinity | 限定任务允许在哪些 CPU 上运行 |
| Kubernetes CPU Manager | 为部分容器分配或绑定 CPU |
| sched_ext | 决定任务怎样排队、选择 CPU 和获得执行机会 |
Kubernetes 的 kubelet 和容器运行时依赖 Linux cgroup 对 Pod、容器的 CPU 和内存请求及限制进行资源管理。(Kubernetes)
但加载自定义 BPF 调度器后,不能默认认为原有 CPU 控制行为会完全保持不变。
当前 Linux cgroup v2 文档明确指出:
cpu.weight是否影响 BPF 调度器,取决于调度器是否实现相应的cgroup_set_weight回调;cpu.max在文档中被标注为作用于 fair-class 调度器;- 部分 CPU 统计项同样区分 fair-class 任务和 BPF 调度器任务。(Linux内核文档)
这意味着在 Kubernetes 节点上测试 sched_ext 时,至少要验证:
| 检查项 | 需要回答的问题 |
|---|---|
| CPU Request | 不同 Pod 的相对权重是否仍符合预期 |
| CPU Limit | 超过限额后是否按照预期限制 CPU 时间 |
| Guaranteed Pod | 独占 CPU 是否仍然保持隔离 |
| CPU Manager | 自定义策略是否尊重 cpuset 范围 |
| Topology Manager | CPU 选择是否破坏 NUMA 和设备亲和性 |
| 系统进程 | kubelet、容器运行时和内核线程是否会被饿死 |
| 监控统计 | 原有 CPU 使用率和限流指标是否仍然可信 |
sched_ext 核心会拒绝超出任务合法 CPU 掩码的选择,因此 BPF 调度器不能随意把任务放到不允许使用的 CPU 上。(Linux内核文档)
但“没有越过 cpuset”不等于“容器调度行为完全正确”。
生产调度器仍然需要理解 cgroup 层级、任务权重和不同 QoS 类型,否则它可能在内核层合法运行,却在业务层破坏资源隔离。
八、如何进行一次最小化实验
不建议直接在唯一一台生产服务器上运行陌生的 sched_ext 调度器。
默认情况下,BPF 调度器加载后可能接管系统中的 SCHED_NORMAL、SCHED_BATCH、SCHED_IDLE 和 SCHED_EXT 任务,而不只是运行压测程序的单个进程。只有启用部分切换模式时,才会仅接管显式设置为 SCHED_EXT 的任务。(Linux内核文档)
因此,试验环境最好使用:
- 独立测试服务器;
- 可以随时重建的测试节点;
- 从 Kubernetes 集群中隔离的节点;
- 有远程控制能力的物理机;
- 没有关键数据的虚拟机。
1. 检查内核版本和配置
uname -r
grep -E 'CONFIG_(BPF|BPF_SYSCALL|BPF_JIT|DEBUG_INFO_BTF|SCHED_CLASS_EXT)=' \
/boot/config-$(uname -r)
部分系统会把内核配置放在 /proc/config.gz:
zgrep -E 'CONFIG_(BPF|BPF_SYSCALL|BPF_JIT|DEBUG_INFO_BTF|SCHED_CLASS_EXT)=' \
/proc/config.gz
上游文档列出的关键配置包括:
CONFIG_BPF=y
CONFIG_SCHED_CLASS_EXT=y
CONFIG_BPF_SYSCALL=y
CONFIG_BPF_JIT=y
CONFIG_DEBUG_INFO_BTF=y
2. 编译上游示例
在对应的 Linux 内核源码根目录中执行:
make -j"$(nproc)" -C tools/sched_ext
随后启动最基础的示例调度器:
sudo tools/sched_ext/build/bin/scx_simple
scx_simple 是一个用于理解机制的简单调度器,不应该因为它能够运行,就直接把它当作生产优化方案。官方示例仓库同样强调,示例调度器主要用于演示和原型开发。(GitHub)
3. 检查运行状态
cat /sys/kernel/sched_ext/state
cat /sys/kernel/sched_ext/root/ops
cat /sys/kernel/sched_ext/enable_seq
正常情况下可以看到类似结果:
enabled
simple
1
其中:
state表示当前是否启用了 BPF 调度器;root/ops表示当前调度器名称;enable_seq表示系统启动后成功加载调度器的次数。
还可以查看当前调度器暴露的事件计数:
cat /sys/kernel/sched_ext/simple/events
这里可以发现:
- CPU 选择是否频繁回退;
- 本地 DSQ 目标 CPU 是否离线;
- 调度器是否进入旁路模式;
- 是否发生了不合理的重复入队;
- 默认时间片是否被频繁补充。
这些状态和事件接口已经由上游内核文档提供。(Linux内核文档)
4. 主动回退
对于简单示例,可以在运行调度器的终端按下 Ctrl+C。
用户态进程退出后,BPF 调度器会被卸载,系统恢复默认调度器。
需要提前验证的不是“理论上能够回退”,而是:
- 回退需要多久;
- 回退期间是否产生延迟抖动;
- 进程异常退出后能否自动恢复;
- 调度器卡住时看门狗是否有效;
- 系统启动时加载失败是否影响业务;
- 配置更新失败后能否保留旧版本。
九、调度器压测不能只看 CPU 使用率
如果只比较 CPU 使用率,很容易得出错误结论。
调度器优化应该同时覆盖业务、操作系统和硬件三个层次。
业务指标
| 指标 | 观察目的 |
|---|---|
| 吞吐量 | 是否处理了更多任务 |
| P50 延迟 | 普通请求是否改善 |
| P99 和 P999 | 长尾延迟是否降低 |
| 超时率 | 关键请求是否更稳定 |
| 错误率 | 是否因调度变化出现异常 |
| 单请求 CPU 成本 | 是否只是用更多 CPU 换取延迟 |
调度指标
| 指标 | 观察目的 |
|---|---|
| Run Queue 等待时间 | 线程获得 CPU 前等待了多久 |
| 上下文切换次数 | 是否产生额外切换成本 |
| CPU Migration | 线程是否频繁跨核迁移 |
| 任务饥饿时间 | 某类任务是否长期得不到运行 |
| 调度器回退次数 | BPF 策略是否不稳定 |
| DSQ 队列长度 | 哪类任务出现了积压 |
硬件指标
| 指标 | 观察目的 |
|---|---|
| LLC Cache Miss | CPU 迁移是否破坏缓存局部性 |
| NUMA Remote Access | 是否频繁访问远端内存 |
| IPC | 每个 CPU 周期完成的指令数量 |
| 内存带宽 | 后台任务是否挤压关键任务 |
| CPU 频率 | 调度策略是否影响能耗策略 |
| 整机功耗 | 性能提升是否带来更高能源成本 |
测试至少要覆盖四种负载:
- 只有关键请求;
- 只有后台任务;
- 两类任务混合运行;
- 超过系统容量的过载场景。
只有关键请求时表现良好并不够。
真正困难的是:当批处理、日志、监控和在线流量同时抢占 CPU 时,调度器能否保护关键路径,同时避免后台任务永久饥饿。
十、推荐的灰度发布路径
调度器位于整台节点的基础设施层,因此灰度单位应该是节点,而不是普通应用实例。
flowchart LR
A[默认调度器基线] --> B[单台测试节点]
B --> C[故障与回退测试]
C --> D[少量灰度节点]
D --> E[同规格节点对照]
E --> F{业务和系统指标正常}
F -->|否| G[卸载策略并回退]
F -->|是| H[按节点分批扩大]
建议按照以下顺序推进。
第一阶段:离线验证
验证程序能够加载、卸载,并确保:
- BPF Verifier 可以通过;
- 配置错误不会阻塞启动;
- 进程退出后能够恢复;
- 看门狗可以处理任务停滞;
- 调试转储能够被采集;
- 不会影响 SSH 和系统管理进程。
第二阶段:固定压测
在同一台机器、同一个内核、同一套业务版本上,分别运行:
- 不加载
sched_ext的默认调度器; - 加载目标 BPF 调度器。
这样可以最大限度排除硬件、内核和业务版本差异,只比较调度策略本身。
第三阶段:影子节点
复制真实流量特征,但不承载关键业务结果。
重点观察:
- 长尾延迟;
- CPU 迁移;
- 缓存未命中;
- cgroup 权重;
- CPU 限额;
- 系统进程响应能力。
第四阶段:小规模生产灰度
选择少量同规格服务器,建立明确的对照组。
不要只比较灰度前后的平均值,还要排除流量类型、机型、NUMA 拓扑和请求复杂度差异。
第五阶段:自动回退
为调度器设置独立健康判断。
例如,在下列情况发生时自动停止用户态调度器进程:
- P99 延迟持续恶化;
- SSH 或节点心跳异常;
- 任务停滞事件增加;
- 调度器进入旁路模式;
- CPU 迁移数量异常增长;
- 系统进程运行延迟超过阈值;
- Kubernetes 节点变为 NotReady。
回退条件必须依赖节点级指标,不能只看某一个业务接口。
十一、哪些场景值得使用 sched_ext
sched_ext 最适合那些已经确认存在调度瓶颈,并且通用参数无法解决问题的系统。
适合考虑的场景
在线服务与批处理混部
需要优先保护用户请求,同时利用空闲 CPU 执行后台任务。
长尾延迟高度敏感
平均延迟已经很好,但 P99、P999 仍然影响业务结果。
复杂 NUMA 和缓存拓扑
线程迁移、远端内存和缓存失效已经成为主要性能成本。
GPU、加速卡和 CPU 协同
GPU 经常因为 CPU 数据准备、通信或调度延迟而等待。
超大规模基础设施
单台服务器只有 1% 的优化收益,但整个机群可以节省大量成本。
调度算法研究与验证
需要快速测试新的公平性、优先级、节能或者任务分组策略。
不适合贸然使用的场景
尚未证明问题出在调度器
如果真正瓶颈是数据库、锁、网络或者磁盘,更换 CPU 调度策略不会解决问题。
普通低流量业务
维护自定义调度器的成本可能远高于增加少量服务器。
缺少节点级可观测性
无法观察运行队列、迁移、缓存和 NUMA 指标时,很难判断策略是否真正有效。
依赖严格 cgroup CPU 限制
在没有完整验证权重和限额行为前,不应直接接管容器生产节点。
要求硬实时保证
sched_ext 不应该被简单视为 SCHED_DEADLINE、实时调度类或者 PREEMPT_RT 的替代品。
没有自动回退机制
人工登录服务器停止调度器,不是可靠的生产回滚方案。
判断是否值得深入这一层,可以先问三个问题:
请求慢的时候,线程是否真的在等待 CPU?
调度等待、CPU 迁移或者缓存失效是否已经成为主要成本?
这种成本能否通过线程池、CPU Manager、cgroup 和绑核等简单手段解决?
只有前两个问题答案为“是”,第三个问题答案为“否”,才值得进一步研究自定义调度器。
十二、sched_ext 真正带来的改变
过去,CPU 调度器属于少数内核开发者才能深入修改的区域。
一个新的调度策略从开发到生产,往往需要经历:
- 修改内核;
- 重新编译;
- 重启机器;
- 长时间稳定性测试;
- 等待内核版本升级;
- 在大规模机群中缓慢部署。
sched_ext 没有让 CPU 调度变得简单,也没有让所有开发者都必须编写调度器。
它真正改变的是调度策略的交付方式。
调度器仍然运行在操作系统最关键的路径中,但策略可以通过经过验证的 BPF 程序动态加载;实验失败时,可以退出程序并恢复默认调度器;业务团队也可以把延迟等级、任务类别、缓存拓扑和负载状态编码进调度策略。
这使 CPU 调度从一种固定的内核能力,逐渐变成一层可部署、可观察、可灰度和可回退的基础设施策略。
未来的性能优化可能不再只是:
- 增加服务器;
- 扩大线程池;
- 修改 JVM 参数;
- 优化 SQL;
- 增加缓存;
- 调整容器 CPU Limit。
当系统规模足够大、长尾延迟足够重要时,团队还可以继续向下追问:
哪些线程应该先获得 CPU?
哪些任务应该共享缓存?
哪些后台工作可以主动让路?
CPU 调度是否应该理解业务关键路径?
而 sched_ext 提供了一种不必永久维护私有内核分支,也能开始回答这些问题的新方式。

评论区 0