essay

GMP中的用户态和内核态

#OS

为什么用户态 ↔ 内核态切换开销大?(在 GMP 调度模型)

一、先简单说:什么是用户态 / 内核态

  • 用户态:应用代码跑的地方(你的 Go 程序、业务逻辑)
    权限低,不能直接操作硬件、页表、线程。
  • 内核态:操作系统内核跑的地方
    管线程、内存、网络、磁盘,权限最高。

当你调用:

  • syscall(系统调用)
  • read/write/sleep/mutex
    都会触发 用户态 → 内核态 → 用户态 的切换。

二、切换开销到底大在哪?
1. 上下文要完整保存与恢复
切换时 CPU 必须:

  • 保存用户态寄存器、栈、指令指针
  • 加载内核态栈、寄存器、页表、权限
  • 执行完再切回去,又要恢复一遍

这不是简单跳转,是两套执行环境的完整交换

  1. 页表切换 + TLB 刷新(最耗性能)
  • 用户态、内核态用不同虚拟地址空间
  • 切换会导致 TLB(页表缓存)失效
  • 之后访问内存都要重新查表,速度暴跌

TLB 失效一次还好,高并发下频繁切换就是灾难。

  1. CPU 流水线被打断
    现代 CPU 靠流水线、预测执行提速。
    模式切换会:
  • 清空流水线
  • 废掉预取的指令
  • 废掉分支预测

相当于 CPU 要“重新开始”,IPC(每周期指令数)大幅下降。

  1. 内核还要做安全性检查
    内核不会无条件信任用户态:
  • 校验指针合法性
  • 检查权限
  • 拷贝数据(用户栈 ↔ 内核栈)

这些都是纯额外开销

三、放到 GMP 调度模型里:为什么 Go 要尽量避免系统调用?
这就是 GMP 最核心的设计动机之一。

  1. 传统线程(OS thread)的痛点
    如果一个线程进入系统调用(比如 sleep、read):
  • 用户态 → 内核态
  • 线程阻塞在内核
  • OS 把线程挂起 → 再次切换、再次调度

一次 IO = 至少两次模式切换 + 一次线程调度
高并发下成千上万次切换 → CPU 空转严重。

2. GMP 如何解决?
Go 做了一件关键事情:
把“阻塞”留在用户态,不轻易进内核态阻塞 M

  • G(goroutine)阻塞时(channel、wait、sleep)
  • 不执行系统调用、不切内核态
  • 只在 Go 运行时(用户态)把 G 挂到等待队列
  • P 直接抢下一个 G 继续跑

整个过程完全在用户态完成,0 内核切换开销。

  1. 只有万不得已才进内核
    只有真正需要 OS 协助时才进入内核态:
  • 真正的系统调用(syscall)
  • 网络 IO、文件 IO
    GMP 会做 hand off P
  • M 进入内核阻塞
  • P 去找新的 M 继续运行 G
    避免整个 P 被卡住。

为什么用户态内核态切换开销大?
因为涉及上下文保存恢复、页表切换、TLB 刷新、CPU 流水线清空、安全性校验,是一套重量级的 CPU 环境切换,高并发下会产生巨大性能损耗。

放在 GMP 里的意义:
Go 的 GMP 调度器尽可能在用户态完成 G 的挂起、唤醒、切换,避免频繁进入内核态阻塞 OS 线程,从而消除大量模式切换开销,实现高并发、轻量调度。

comments如果有不同意见或者补充,直接留在这里。
contact

在别处继续找到我

如果你想聊技术、设计,或者只是打个招呼。

暂未配置外部链接