是,但非抢占而是解绑复用;syscall中g释放p让其他m接管,内核态不中断,exitsyscall才是强制重调度点,确保不卡死。

Go 协程在系统调用(syscall)中不会被“抢占”,而是被“解绑 + 复用”——sysmon 不会强行中断内核态的 read、epoll_wait 或 select,它只会在用户态返回时接管调度权。真正的动作不是抢占,是回收空闲的 P 并分配给其他 M。
为什么 syscall 中的 G 看似“不可打断”,但程序却不卡死?
因为 Go 调度器根本没试图打断内核态执行。当一个 G 进入阻塞式系统调用(如 read、netpoll),它会主动释放绑定的 P,让该 P 可被其他 M 拿去运行别的 G。此时原 M 会进入休眠等待系统调用返回,而 P 已脱手。
-
sysmon每约 20ms 检查一次所有P的状态,若发现某个P处于_Psyscall状态且已超时(默认 ≥10ms),就会标记该P可被“retake” - 所谓
retake,不是把正在内核里等read返回的G强行拉出来,而是把空闲的P从原M手中拿走,交给另一个空闲的M使用 - 原
G仍卡在内核,但它的P已被复用,其他G可照常运行——这才是不卡死的关键
exitsyscall 是真正的调度点,不是 sysmon
当系统调用终于返回用户态,控制流不会直接回到原 G 的下一条指令,而是先进入 runtime.exitsyscall。这个函数才是决定是否让出 P 的地方。
- 如果此时有其他
G在本地队列或全局队列等待,且当前P没被标记为“必须还给原M”,exitsyscall会直接调用gopreempt_m,把刚返回的G放回队列,切换执行别的G - 这个切换是同步、确定的,不依赖信号,也不受内联/汇编/Cgo 影响
- 也就是说:
syscall本身不触发抢占,但exitsyscall是强制重调度点——哪怕你刚从一个 5s 的select返回,也大概率立刻让出P
常见误判:GODEBUG=schedtrace=1000 显示 “G waiting” 就代表卡住了?
不一定。看 schedtrace 输出里的 gwait 值要结合状态码判断:
- 如果某
G长期显示gwait=1且状态是_Gwaiting(比如在chan recv上),那是正常阻塞 - 但如果它状态是
_Grunning且gwait持续飙升(比如每秒涨几百),说明它本该被调度却没被切走——大概率是进入了无安全点的纯计算循环,或LockOSThread后屏蔽了信号 - 而
_Gsyscall状态下的G即使gwait很高,也不代表问题;它只是在等内核返回,P早就不归它了
真正容易被忽略的是:retake 和 exitsyscall 是两套机制,前者管 P 的复用,后者管 G 的重调度。它们协同工作,但作用域完全不同。写网络服务时,别指望靠 sysmon 把卡在 accept 里的 G 拉出来——它本来就不该在那里久留,而应靠 net.Conn.SetDeadline 或 context.WithTimeout 主动控制阻塞边界。











