TUN inbound (Darwin): Wait() blocks via kqueue instead of busy-spinning - #6580
TUN inbound (Darwin): Wait() blocks via kqueue instead of busy-spinning#6580Jidos86 wants to merge 4 commits into
Conversation
DarwinTun.Wait() was procyield(1) -- a CPU-yield hint, not a real blocking wait. stack_gvisor_endpoint.go's dispatchLoop (a single dedicated goroutine) calls ReadPacket() then Wait() in a tight loop with no other throttling whenever the tun's non-blocking fd has nothing to read, so this pinned a full CPU core for the entire connected lifetime of the tunnel, independent of actual traffic volume -- observed causing severe, sustained device heating on a real iPhone 16 Pro (severe enough that iOS's own thermal management disabled the camera flash). tun_android.go builds its link endpoint via gVisor's own fdbased.New(...) -- a properly blocking fd-based endpoint -- and never had this issue. This adds a kqueue registered for EVFILT_READ on the tun fd, and has Wait() genuinely block on it (1s bounded timeout, so a racing Close() stays responsive) instead of yielding and immediately re-looping. Falls back to the original procyield behavior if kqueue setup ever fails, so this can only make things better or leave them unchanged, never worse. Verified locally: applies cleanly at this commit, and the patched package (proxy/tun) plus the whole dependent libXray module cross-compile successfully for GOOS=darwin GOARCH=arm64. Not a gVisor-internals expert -- there may be a more elegant fix (perhaps fdbased could be adapted for Darwin the way Android uses it, if the fd differences allow it). This is what fixed the issue in real-device testing; a maintainer may well prefer a different approach. Found and fixed with the help of Claude (Anthropic's AI coding assistant). Fixes XTLS#6579
|
procyield 这里大抵是被滥用了 该删掉的 |
Reviewer feedback (Fangliding): the kqueue-setup-failure fallback still called procyield(1) -- the exact busy-spin this whole change exists to remove, just gated behind an edge case (kqueue setup failing, which practically never happens on a real Darwin system) instead of always. A genuine time.Sleep actually yields the CPU for a bounded duration, unlike procyield's near-instant scheduler hint, which would let the tight dispatchLoop caller (stack_gvisor_endpoint.go) spin just as hot as before if this path were ever actually hit. The now-unused //go:linkname procyield declaration is removed too rather than left as dead code. Verified via local cross-compile (darwin/arm64 and ios/arm64) -- go build/go vet both clean.
…usy-spin # Conflicts: # proxy/tun/tun_darwin.go
Overall, the reported problem is real and the main direction is sound: replacing Blocking findings
Missing coverageThe existing CI is green, and I independently verified
Minor documentation issue: the PR body still says setup failure falls back to After the fd lifecycle, error handling, and tests are addressed, I think this is a good fix and should be mergeable. |
总体上,问题真实,主要修复方向也正确:用 level-triggered 的 阻塞问题
缺少的测试现有 CI 全部通过;我也独立验证了
另有一个较小的文档问题:PR 描述仍写着创建失败后回退到 完成 fd 生命周期、错误处理和测试后,我认为这会是一个值得合并的修复。 |
…t kevent errors, add tests - waitKq is now a dedicated waitKqueue type (atomic closed flag + sync.Once) instead of a bare int fd -- Close() and a racing Wait() could otherwise double-close or operate on a since-reused fd number (P1). - wait() reports persistent kevent failures (anything but EINTR) as false; Wait() permanently falls back to the sleep path on that signal instead of retrying a syscall that's already shown it won't succeed, which would silently reintroduce the exact busy-spin this change removes (P2). - Added proxy/tun/tun_darwin_test.go coverage for all six scenarios requested: blocking with no data, wake on readable fd, timeout, Close() waking a blocked wait, concurrent/repeated close, and persistent kevent failure without spinning -- via unix.Socketpair, no real utun/root needed.
|
Thanks for the thorough review -- both P1 and P2 were real gaps, appreciate the specific repro on the double-close/fd-reuse case. Pushed a fix (69d7bb2):
Also re-verified |
Fixes #6579.
What
DarwinTun.Wait()wasprocyield(1)-- a CPU-yield hint, not a real blocking wait.stack_gvisor_endpoint.go'sdispatchLoop(a single dedicated goroutine) callsReadPacket()thenWait()in a tight loop with no other throttling whenever the tun's non-blocking fd has nothing to read, so this pinned a full CPU core for the entire connected lifetime of the tunnel, independent of actual traffic volume.For comparison,
tun_android.gobuilds its link endpoint via gVisor's ownfdbased.New(...)-- a properly blocking fd-based endpoint -- and never had this issue.Impact
Observed causing severe, sustained device heating on a real iPhone 16 Pro during active tunnel connections -- severe enough that iOS's own thermal management disabled the camera flash ("iPhone needs to cool down before using flash"). See #6579 for the full writeup.
The fix
Adds a kqueue registered for
EVFILT_READon the tun fd, and hasWait()genuinely block on it (1s bounded timeout, so a racingClose()stays responsive) instead of yielding and immediately re-looping. Falls back to a boundedtime.Sleep(1ms)if kqueue setup ever fails, or once the kqueue reports a persistent (non-EINTR)keventfailure at runtime, so this can only make things better or leave them unchanged, never worse.Testing
proxy/tunpackage (and the whole dependent libXray module) cross-compiles successfully forGOOS=darwin GOARCH=arm64andGOOS=ios GOARCH=arm64.proxy/tun/tun_darwin_test.gocoverage (viaunix.Socketpair, no real utun/root needed) for: blocking with no data, waking on a readable fd, timeout,Close()waking a blocked wait, concurrent/repeated close, and a persistentkeventfailure not causing a spin.Update (review round 1)
Addressed both correctness gaps @yiguodev flagged:
waitKqis now a dedicatedwaitKqueuetype (atomicclosedflag +sync.Once) instead of a bareint, soClose()and a racingWait()can no longer double-close or operate on a since-reused fd number.keventerrors: no longer discarded. A persistent (non-EINTR) failure now permanently switchesWait()to thetime.Sleepfallback instead of retrying the same broken kqueue forever, which would have silently reintroduced the exact busy-spin this PR removes.Note
This was found and fixed with the help of Claude (Anthropic's AI coding assistant), while debugging a real thermal-management report on iOS. I'm not a gVisor internals expert -- there may well be a more elegant fix (maybe even adapting
fdbasedfor Darwin the way Android uses it, if the fd differences allow it). I'm just sharing what fixed the issue in my own testing, not claiming this is the right solution -- happy for a maintainer to take a completely different approach.