Syscall Handling — Interview Questions¶
Table of Contents¶
- How to Use This Page
- Intern / Junior Questions
- Middle Questions
- Senior Questions
- Staff Questions
- Coding Exercises
- Whiteboard Questions
- Tips for Candidates
How to Use This Page¶
This page collects interview questions about Go's syscall handling, ranged by level. Use it to:
- Prepare for an interview: read the questions, draft your answers, compare with the suggested points.
- Conduct an interview: pick 3–5 questions per level; do not ask all of them.
- Self-assess: if you struggle on the middle questions, revisit middle.md.
Each question lists key points an answer should hit. There is no single right answer; depth matters more than exact wording.
Intern / Junior Questions¶
Q1: What is a system call?¶
Key points: - A request from user-space code to the kernel. - Switches the CPU to kernel mode (via the syscall instruction on x86-64). - The calling thread is paused until the kernel returns. - Examples: read, write, open, close.
Q2: What happens to a goroutine when it makes a blocking syscall?¶
Key points: - The goroutine itself is paused; the M is in the kernel. - The runtime detaches the P from the M so other goroutines can run on a different M. - This is called the "P handoff". - When the syscall returns, the M tries to re-attach to a P; if none is free, the M parks.
Q3: Why does a Go program with GOMAXPROCS=4 sometimes have more than 4 threads?¶
Key points: - GOMAXPROCS controls the number of Ps (concurrent execution units), not Ms. - Each blocking syscall holds an M but releases the P. - So you may have GOMAXPROCS Ms running Go code plus extra Ms in syscalls or cgo. - Plus sysmon (one M), plus parked Ms in the pool.
Q4: What is the difference between network I/O and file I/O from the runtime's view?¶
Key points: - Network I/O uses the netpoller (epoll/kqueue/IOCP). Non-blocking; parks the goroutine without holding an M. - File I/O uses the blocking-syscall path. Holds an M for the duration. - This is why 50 000 TCP connections cost almost nothing, but 50 simultaneous large file reads cost 50 threads.
Q5: What is sysmon?¶
Key points: - A background goroutine that runs on its own M without a P. - Runs periodically (every 20 µs to 10 ms). - Tasks: trigger goroutine preemption, hand off Ps from syscalling Ms, force GC.
Middle Questions¶
Q6: Walk me through entersyscall step by step.¶
Key points: - Mark G as _Gsyscall. - Save the G's PC and SP for later inspection. - Detach the P from the M (clear m.p, set m.oldp). - Mark the P as _Psyscall. - Notify sysmon if it was sleeping. - Disable preemption via m.locks++ during the transition.
Q7: When does sysmon hand off a P that is in a syscall?¶
Key points: - After the P has been in _Psyscall for > 10 µs. - AND the P has runnable Gs, OR no spinning Ms exist (so handoff is needed for parallelism). - Sysmon CAS-flips pp.status from _Psyscall to _Pidle and calls handoffp.
Q8: What is the fast path vs the slow path in exitsyscall?¶
Key points: - Fast: CAS oldp.status from _Psyscall to _Pidle, re-attach. Runs entirely in user space. - Slow: if oldp was handed off (CAS fails), try to grab any idle P. If no P available, the M parks and the G goes on a runqueue. - Fast path is ~50 ns; slow path is ~1 µs.
Q9: Why does cgo behave like a blocking syscall?¶
Key points: - The C function runs on the M for an unknown duration. - The M cannot be reused for other goroutines while in cgo. - The runtime calls entersyscall before the C function, detaching the P. - C may have thread-local state, install signal handlers, etc., so the M cannot be migrated.
Q10: What is _Psyscall?¶
Key points: - A P state where the M is in a syscall. - The M is still nominally attached to the P (via m.oldp), but the P is available for handoff. - Sysmon checks for _Psyscall Ps and hands them off after 10 µs.
Q11: What is the netpoller doing on Linux?¶
Key points: - Maintains an epoll file descriptor. - Each network fd is registered with EPOLLIN | EPOLLOUT | EPOLLET. - When a goroutine does a non-blocking read that returns EAGAIN, it parks via the netpoller. - The scheduler periodically calls epoll_wait to find ready fds and unpark waiting goroutines.
Q12: Why is epoll not enough for file I/O?¶
Key points: - On Linux, epoll on regular files always reports them as "ready". - Real disk reads still block in the kernel. - So Go cannot use epoll for files; it falls back to the blocking-syscall path. - io_uring (kernel 5.1+) could solve this, but Go does not use it as of Go 1.22.
Q13: What is a VDSO syscall?¶
Key points: - A "syscall" implemented in user space, in code mapped by the kernel into every process. - Examples: clock_gettime, gettimeofday, getcpu. - The Go runtime uses VDSO for time.Now(), etc. — no entersyscall overhead.
Senior Questions¶
Q14: A Go service in a Kubernetes pod with cpu: 500m is using 10× more CPU than expected. What might be wrong?¶
Key points: - GOMAXPROCS may be set to the node's CPU count instead of the container's quota. - On Go 1.16+ Linux, the runtime reads cgroup v2 quota correctly. Earlier versions need automaxprocs. - Symptoms: scheduler thrash, GC variability, high context switches. - Fix: log runtime.GOMAXPROCS(0) at startup; verify it matches CPU quota.
Q15: Your cgo-heavy service is exceeding pids.max in a container and panicking. How do you fix it?¶
Key points: - Cgo calls hold Ms; unbounded concurrency causes M explosion. - Bound cgo concurrency with a semaphore (channel) before each cgo call. - Alternative: a worker pool with a fixed number of pinned goroutines. - Long-term: batch cgo calls, reduce the number of underlying calls.
Q16: A file-I/O heavy workload shows climbing thread count and degraded latency. Diagnose.¶
Key points: - File reads do not use the netpoller; each holds an M. - If concurrency is unbounded, you spawn one M per in-flight read. - Tail latency rises because the kernel queues I/O at the device. - Fix: bound concurrency with a semaphore (sized to disk parallelism, ~4–16).
Q17: When should you use LockOSThread?¶
Key points: - For OS APIs that are thread-scoped: setns, prctl, sched_setaffinity. - For thread-affine C libraries: OpenGL, CUDA, GnuTLS. - For long-lived workers in a cgo pool (ensures the M is stable for the C library). - NOT for goroutine identity (use context.Context instead). - NOT for general concurrency control (use channels/mutexes).
Q18: What happens if a LockOSThread'd goroutine exits without unlocking?¶
Key points: - The runtime treats the M as compromised (because the locked goroutine may have modified OS state). - The M is destroyed, not pooled for reuse. - Repeated lock-without-unlock leaks Ms steadily. - Mitigation: defer runtime.UnlockOSThread() immediately after LockOSThread.
Q19: Why might time.Now() be slow on some systems but fast on others?¶
Key points: - On Linux/x86-64, time.Now() is a VDSO call (~20 ns). - Some containers hide the VDSO; falls back to real syscall (~300 ns). - On older kernels or unusual platforms (no VDSO), it is a real syscall. - Older virtualization (without kvm-clock) used to be slower; rare today.
Q20: A service does os.ReadFile in 1000 goroutines and you see ~500 threads. Is this normal?¶
Key points: - File reads hold an M each. 1000 reads → up to 1000 Ms briefly. - Reaching 500 suggests reads complete fast enough that the M pool churns. - Below GOMAXPROCS plus some headroom is the "active" set; the rest is in syscalls. - Not necessarily broken; just bounded by the M pool dynamics. - If sustained, fix with a semaphore.
Staff Questions¶
Q21: Design a service that does heavy disk I/O, heavy cgo, and serves 10 000 concurrent HTTP requests. Discuss thread/goroutine layout.¶
Key points: - Three lanes: HTTP (netpoller, unbounded goroutines), file I/O (semaphore-bounded), cgo (pinned worker pool). - File I/O semaphore: ~disk parallelism (4–16). - Cgo workers: GOMAXPROCS to ~2× depending on whether cgo is CPU-bound. - HTTP handlers dispatch to lanes via channels. - Backpressure: shed load at HTTP level when lanes are full. - Monitor /sched/threads:threads, /sched/goroutines, p99 latency per lane.
Q22: Explain the lock-free CAS between sysmon's handoff and exitsyscallfast.¶
Key points: - Both sides try to CAS pp.status from _Psyscall to _Pidle. - Only one wins. - If sysmon wins: hands off via handoffp; the syscalling M takes the slow path on return. - If exitsyscall wins: re-attaches oldp; fast path completes. - No sched.lock taken in the fast case. - This is what makes high-volume short syscalls cheap.
Q23: A service has thread count climbing slowly over days, eventually OOM. What categories of bug should you investigate?¶
Key points: - LockOSThread without UnlockOSThread — M leak. - Cgo callbacks from C threads that never return — Go runtime spawns Ms to attach to them. - A library spawning its own pthreads (not via Go). - M pool growth from sustained high concurrency. - Tools: pprof.threadcreate, /proc/self/status, GODEBUG=schedtrace.
Q24: How would you implement a "cooperative" yield in user space without kernel involvement, like runtime.Gosched?¶
Key points: - The runtime maintains a runqueue per P. - Gosched requeues the current G at the end of the runqueue and calls schedule(). - schedule() pops the next runnable G and executes it. - This is all user-space scheduling; no syscall involved. - Useful for fairness in tight loops (pre-1.14 era). - Less useful since async preemption (Go 1.14+).
Q25: A goroutine on a network connection appears to "hang" — no data flowing, but the connection is open. What runtime mechanics might be involved?¶
Key points: - The goroutine is parked in the netpoller waiting for fd readiness. - The kernel has not reported the fd as ready. - Possible causes: peer not sending, network drop, TCP keepalive interval long, dead peer with no FIN. - Diagnose with ss -i, tcpdump, application-layer keepalives. - Implement read timeouts via SetReadDeadline to escape the parked state.
Coding Exercises¶
Exercise 1: Bounded file reader¶
Write a BoundedReadFile(paths []string, n int) [][]byte function that reads N files concurrently. Bound the in-flight file syscalls to n. Return results in input order.
Solution sketch:
func BoundedReadFile(paths []string, n int) [][]byte {
results := make([][]byte, len(paths))
sem := make(chan struct{}, n)
var wg sync.WaitGroup
for i, path := range paths {
wg.Add(1)
sem <- struct{}{}
go func(idx int, p string) {
defer wg.Done()
defer func() { <-sem }()
data, _ := os.ReadFile(p)
results[idx] = data
}(i, path)
}
wg.Wait()
return results
}
Discussion: why bound? Because file reads hold Ms. Why preserve order? So the API is intuitive.
Exercise 2: Cgo worker pool¶
Implement a Pool that runs C-style work (just simulated via time.Sleep) on N pinned workers. Each call blocks until a worker is free. Provide a Context-aware API.
Solution sketch: see the worker-pool example in senior.md.
Exercise 3: Thread count tracker¶
Write a goroutine that, every second, logs the current thread count (from /proc/self/status on Linux). Use it to instrument a program that does both file I/O and network I/O. Show the difference in thread growth.
Exercise 4: Detect a leaked M¶
Write a program that intentionally leaks Ms via unbalanced LockOSThread and demonstrate that thread count grows over time. Then add a fix using defer runtime.UnlockOSThread().
Whiteboard Questions¶
W1: Draw the state transitions for a goroutine that does a syscall and gets handed off.¶
Expected diagram: _Grunning → _Gsyscall (entersyscall) → slow path: _Grunnable (on runqueue) → _Grunning (resumed on different M).
W2: Draw the M, P, G layout for a service with 10 000 TCP connections idle, 50 file reads in flight, and 20 cgo calls in flight. GOMAXPROCS=8.¶
Expected: 4 Ps with running Ms (active work), some Ms in syscalls (file reads + cgo), ~10 000 Gs parked in netpoller, ~50 Gs in syscall (file), ~20 Gs in cgo state. Approximate thread count: 8 + 50 + 20 + sysmon + pool = ~80.
W3: Show what happens during entersyscall if GOMAXPROCS=1 and only one G exists.¶
Expected: G goes _Gsyscall. P detaches. P sits idle with empty runqueue. M goes into kernel. sysmon notices but does nothing (no work to do, no need to start another M). When syscall returns, exitsyscall fast path re-attaches. Service runs as if syscall was synchronous from the goroutine's perspective.
Tips for Candidates¶
- Know the two paths: network (netpoller) vs blocking (entersyscall). Many candidates conflate them.
- Use the right vocabulary: G, M, P, handoff,
_Psyscall, sysmon. Sloppy terms confuse interviewers. - Quantify: "10 µs sysmon threshold", "epoll edge-triggered", "10000-thread default cap". Numbers make answers credible.
- Draw: when in doubt, draw the state machine or the M/P/G diagram.
- Connect to systems: tie answers to
topoutput, pprof,GODEBUG=schedtrace. Shows you have actually used this in practice. - Be honest about limits: "I'm not sure of the exact line in runtime, but it's in
proc.gonearretake" is better than fabricating. - For staff-level: emphasize design trade-offs. Why bound to 8 file I/O workers? Because disk parallelism is ~8. Why pin cgo workers? Because the C library has thread-local state. Always have a "why".