System Design Cases
Operating System Basics for Architects
Operating System Basics for Architects: process vs thread, userspace vs kernelspace, syscalls, virtual memory, file descriptors, CFS scheduler, signals, IPC primitives (pipes/shm/mq/unix), epoll/kqueue/io_uring async I/O models. Concept page.
Operating-system basics for service architects
An application runtime does not replace the operating system. It schedules language-level tasks onto OS threads, crosses system-call boundaries for privileged operations, and consumes kernel-managed resources with finite ownership and lifetime.
Processes, threads and runtime schedulers
A process owns an address space, descriptor table and other execution state. Threads in that process share the address space and descriptor table while carrying distinct scheduling state and stacks.
Runtime concurrency models sit above this boundary. The Go runtime, for example, represents goroutines as Gs, worker threads as Ms, and execution resources as Ps. There are GOMAXPROCS Ps and potentially many Ms because threads may block in system calls. Calling Go a “single-thread event loop” loses the behavior that matters for CPU saturation, blocking calls and debugging.
File descriptors are references, not files
A descriptor is a small process-local integer that refers to an open file description. Duplicated descriptors and descriptors inherited across fork() may refer to the same open description and therefore share file offset and status flags. Closing one descriptor removes one reference; it does not invalidate all other references.
Every descriptor-producing operation needs explicit ownership on success and on every error path. Use atomic close-on-exec variants such as open(..., O_CLOEXEC) where a descriptor must not cross an exec boundary. A later fcntl in another step has a race with concurrent fork-plus-exec.
io_uring changes how operations are submitted and completed. It can reduce syscall and context-switch overhead for suitable workloads. It does not determine who closes a socket, prevent inheritance, or fix an ownership leak.
fork and exec are different operations
fork() creates a child process. On Linux, memory pages are normally copy-on-write, and inherited descriptors refer to the corresponding open file descriptions. In a multithreaded child, the safe pre-exec surface is intentionally narrow.
An exec function does not create another process. It replaces the current process image. The process identity continues, while program text, data and threads are replaced according to the standard. Descriptors remain open unless marked close-on-exec or explicitly closed.
I/O completion is not necessarily durability
Buffered writes can complete after data reaches the page cache. close() releases the descriptor but does not promise that the bytes reached stable media. If the product contract requires durable acknowledgement, use the filesystem/database synchronization protocol that provides it and handle errors. Device caches, filesystem ordering and replicated storage may add further boundaries.
Pressure and failure
Descriptors, threads, memory and I/O queues are finite. An EMFILE incident should trigger admission control and leak diagnosis, not only a higher limit. Preserve descriptors for logging/control where appropriate, cap concurrency, and measure descriptor count by type. For latency, separate runnable delay, syscall time, I/O wait, runtime pauses and downstream service time.
Scenarios
A logical request is scheduled on an OS thread and resolves a socket through the kernel descriptor table.
The Go G/M/P model demonstrates why goroutines do not imply a single OS thread.
fork creates the child and exec replaces the child image without inventing another process identity.
Atomic close-on-exec plus structured ownership prevents descriptor inheritance and leaks.
Buffered visibility and durable persistence are shown as distinct completion points.
Descriptor exhaustion is contained while the real ownership bug is found; async I/O is not presented as a cure.
Operational checklist
- Track process and system descriptor usage, limits, socket states and long-lived owners.
- Profile runtime runnable queues and OS scheduling separately.
- Avoid unbounded thread, goroutine or request creation in front of a finite downstream resource.
- Make descriptor inheritance explicit for every spawned program.
- Define whether an acknowledgement means userspace accepted, kernel buffered, device flushed, filesystem committed or replicated.
Primary sources
- POSIX.1-2024
fork: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fork.html - POSIX.1-2024
exec: https://pubs.opengroup.org/onlinepubs/9799919799/functions/exec.html - Linux
open(2): https://man7.org/linux/man-pages/man2/open.2.html - Linux
execve(2): https://man7.org/linux/man-pages/man2/execve.2.html - Go runtime scheduler structures: https://go.dev/src/runtime/HACKING