syscall.read会卡住整个m,因阻塞系统调用使m脱离调度器控制,导致绑定p无法调度其他g;若未设非阻塞、未调runtime.entersyscall/exitsyscall或错误码处理不全,p将被长期霸占。

直接调用 syscall.Read、syscall.Write 或 Windows 下的 ReadFile 会卡住整个 OS 线程(M),导致绑定在其上的所有 goroutine 暂停调度——这不是并发,是伪并发。
为什么一个 syscall.Read 就能让整个 P “瘫痪”?
Go 的 G-M-P 调度模型中,M 是 OS 线程,P 是逻辑处理器(负责分发 G)。当某个 goroutine 执行未设非阻塞标志的 syscall.Read 时:
- M 进入内核休眠,完全脱离 runtime 控制,无法响应任何抢占信号
- 调度器发现该 M 长期处于
_Psyscall状态(>20μs),会触发sysmon的 retake 机制,把 P 强行解绑为_Pidle - 但 retake 不是万能的:若 goroutine 已调用
runtime.LockOSThread(),P 就会被强绑定,retake 直接跳过——此时 P 真的被“霸占”了 - 更糟的是,如果该 fd 来自 C 代码或
syscall.Open,且没做os.NewFile包装,它压根不走 netpoller,调度器连轮询机会都没有
Unix 和 Windows 下裸 syscall 的非阻塞成本差异极大
想手动绕过标准库、自己搞非阻塞系统调用?你得同时满足三件事,缺一不可:
- Unix:
syscall.SetNonblock(fd, true)+ 错误码检查errno == syscall.EAGAIN || errno == syscall.EWOULDBLOCK - Windows:fd 必须用
FILE_FLAG_OVERLAPPED创建 + 调用WSARecv或ReadFileEx+ 检查GetLastError() == ERROR_IO_PENDING - 无论哪边,都必须显式调用
runtime.Entersyscall()和runtime.Exitsyscall(),否则 runtime 仍按默认阻塞逻辑处理,P 还是会被卡住
漏掉任意一条,就退化成同步阻塞调用——你写的不是“非阻塞”,只是“假装非阻塞”。
标准库的 *os.File 和 net.Conn 为什么安全?
它们不是“不阻塞”,而是把阻塞封装进了 runtime 的 netpoller 里,让 goroutine 自己挂起,而不是拖垮 M:
-
os.OpenFile("x.log", os.O_RDONLY, 0)返回的*os.File,其Read()方法底层自动注册到 epoll/kqueue/IOCP,G 挂起后 M 可立即跑别的 G - 哪怕是对管道、tty 等设备文件,只要 fd 是 Go 自己打开的(不是从 C 传入),就能进 netpoller
- 若必须用已有 fd(如 C 传来的 int),可用
os.NewFile(uintptr(fd), "name")包装后再调Read();但注意:Windows 下此方式不支持异步 IO,仍可能阻塞
真正危险的从来不是“能不能写裸 syscall”,而是写了一半——设了非阻塞却忘了 Entersyscall,或忘了错误码分支,或在 Windows 下用了 ReadFile 而不是 ReadFileEx。这些地方 runtime 不报错,只默默让你的并发能力归零。










