System Calls, User Mode and Kernel Mode
How a program asks the kernel for help: user vs kernel mode, the path of one write(), interrupts vs exceptions, and measured costs (about a million write calls against 249 when buffered).
A system call is how a user program asks the kernel for a privileged service: a library wrapper puts the call number and arguments in registers and runs a trap instruction, the CPU switches to kernel mode, and the kernel runs the matching handler. Interviewers expect this: a system call is not a context switch, as the same process continues.
A system call is the controlled doorway from a user program into the kernel: a library wrapper traps the CPU into privileged kernel mode, the kernel runs a handler picked by call number, and the result comes back. Measured here, a million one-byte writes made about a million system calls and took roughly two thirds of a second, while the same bytes in 4 KB chunks took 249 calls and a fraction of a millisecond.
- User mode and kernel mode are CPU privilege states, not separate processes; during a system call the same process continues running kernel code.
- On x86-64 Linux, the C library wrapper executes the syscall instruction, and the kernel indexes a table of handlers with the call number.
- The kernel reports a failed system call as a negative error number, which the C library wrapper turns into -1 with the positive value stored in errno.
- Writing one million bytes one at a time made about 1,000,000 write system calls, while the same bytes in 4 KB chunks took 249 calls.
- An interrupt is asynchronous and comes from hardware, while a system call is synchronous and requested by the program through a trap instruction.
- Through the vDSO, clock_gettime cost 22 to 48 ns per call in one VM test, while the same call forced through the kernel cost 369 to 642 ns.
01Why a program cannot touch the hardware
A running program can add numbers, move bytes around in its own memory and jump to its own code. It cannot read a disk block, send a network packet or change which process runs next. Those actions need privileged CPU instructions, and the processor refuses them unless it is in kernel mode. Ordinary programs run in user mode. The operating system kernel runs in kernel mode.
The names are slightly misleading. Kernel mode is not a separate process or a helper thread waiting somewhere. It is a state of the CPU, and when your program makes a system call, the very same process continues executing, now with privilege and now running kernel code on its behalf. User code cannot access kernel memory at all; the CPU stops any attempt.
The reason for the split is damage control. If any program could write to the disk controller directly, one buggy or hostile program could corrupt every file and every other process. With the split, the kernel is the only code allowed to touch devices, and it checks each request first: is this file descriptor yours, may you read that file, does the buffer you gave actually belong to you? A system call is the one sanctioned doorway between the two modes. Everything a program does that matters outside its own memory, from opening a file to starting a thread, goes through it.
02Following one write() through the doorway
Take write(1, "hi", 2) in C. Your code calls a function named write, but that function lives in the C library, and it is not the system call itself. It is a thin wrapper. The wrapper puts the call number for write and the three arguments into the registers the platform expects, then executes the architecture's trap instruction. On x86-64 Linux that instruction is called syscall; other architectures use differently named instructions. glibc's wrapper does the same: it loads the registers, runs syscall, and treats a result between -4095 and -1 as an error. The trap makes the CPU switch to kernel mode, move onto a kernel stack and save the user registers so it can resume later.
The kernel uses the call number to find the right handler, runs it, and puts a result in a register. Then it returns to user mode. Back in the wrapper, the result is checked: the kernel reports failure as a negative error number, and the wrapper turns that into the C convention you know, storing the positive value in errno and returning -1. Success returns the byte count unchanged. The process stays the same one throughout; if you have not met processes and threads yet, read Processes vs threads first. The Linux man page for intro(2) describes exactly this wrapper-and-errno convention, and syscalls(2) calls the system call the fundamental interface between an application and the kernel.
The diagram follows that path. A call such as clock_gettime can take the dashed side route instead. The kernel maps a small shared library called the vDSO into every process, and for a few calls, such as clock_gettime and time on x86-64 (see vdso(7)), the answer can be computed in user space, so no trap happens. Section 6 measures how much that matters.
03Number in, handler out
Inside the kernel, system calls are identified by small integers, defined per architecture as constants named __NR_something. The kernel keeps a table with one handler per number. Dispatch is an array lookup: check that the number is within range, then jump to the entry. A slot with no real implementation points at a handler that just reports the call is not implemented.
You can talk to this table directly. glibc exposes a function called syscall that takes a raw number, which is also how you call a kernel function that has no wrapper. The numbers are not portable, so the one below is specific to x86-64. Test machine: Linux 6.18 in a sandboxed VM.
import ctypes, os
libc = ctypes.CDLL(None, use_errno=True)
print(libc.syscall(39), os.getpid()) # 39 is getpid on x86-64
r = libc.syscall(9999) # no such call
print(r, ctypes.get_errno(), os.strerror(ctypes.get_errno()))The output was 1503 1503 for the first line (the raw call returned the same value as Python's os.getpid()), and -1 38 Function not implemented for the second. Error 38 is ENOSYS. The wrapper behaviour from section 2 is visible here: the kernel's answer was a negative number, the library handed back -1, and the reason sits in errno. The syscalls(2) page lists a few hundred calls on modern kernels.
04Never trust a pointer from user space
Most system calls take pointers: a buffer to fill, a name to open. The kernel cannot assume those addresses are valid, because user code can pass anything, including garbage or an address inside kernel memory. If the kernel simply followed the pointer and it was bad, the whole machine could crash or leak private data. So kernel code reads and writes user memory through dedicated routines that are prepared to fail, and a bad address becomes an ordinary error for that one call.
Here is a deliberate bad pointer, address 1, passed to read on /dev/zero, which always has bytes to give:
import ctypes, os
libc = ctypes.CDLL(None, use_errno=True)
fd = os.open('/dev/zero', os.O_RDONLY)
print(libc.read(fd, ctypes.c_void_p(1), 10), ctypes.get_errno())It printed -1 14, and errno 14 is EFAULT, "Bad address". Running the same thing under strace showed the raw call as read(3, 0x1, 10) = -1 EFAULT (Bad address). The process kept running afterwards. At end of file the kernel has nothing to copy and never touches the pointer, so read on /dev/null returns 0 with no error. So EFAULT shows up only when the kernel actually tries to use the address.
The kernel's approach is to copy through special routines and handle the fault if one happens, rather than validating every pointer first. What the experiment shows is enough: a bad user pointer fails one call and nothing else.
05Interrupts, exceptions and system calls
The system call is one of three ways the CPU ends up in kernel code, and they differ in who triggers them.
An interrupt is asynchronous. A network card has received a packet, a disk finished a transfer, a timer expired. The CPU stops what it was doing between instructions, runs the kernel's handler for that device, and then resumes your program, which never knew. The timer interrupt is how the kernel regains control from a program that never calls into it, and so it drives preemptive scheduling (see Context switching).
An exception is synchronous and caused by the instruction itself: dividing by zero, executing an illegal instruction, touching a page that is not in memory. Some exceptions are fatal for the process. A page fault often is not: the kernel loads the missing page and re-runs the same instruction, and the program continues.
A system call is also synchronous, but it is intended. The program asks for the kernel on purpose by executing the trap instruction (syscall on x86-64), which some texts call a software interrupt or trap. That is the key contrast for an interview answer: interrupts arrive from outside, unexpected, at any moment; system calls come from inside, at a place the programmer wrote.
One more distinction avoids a common mix-up. A system call is not a context switch. The kernel runs inside the same process, with no change of address space. A switch to another process can follow, but only if the call blocks, for example reading from a socket with no data yet.
06What a crossing costs, and how to avoid it
Entering and leaving the kernel is real work, and it adds up when done a million times. Two experiments, both run on a 2-core sandbox VM running Linux 6.18 on x86-64. Each was measured on one machine in a few runs, so the absolute numbers are specific to it: yours will differ, easily by a factor of two or more, because virtualization, security mitigations and CPU model all move them. The ratios are the useful part.
First, writing one million bytes to /dev/null, either one byte per os.write or in 4096-byte chunks:
import os, time
fd = os.open('/dev/null', os.O_WRONLY)
t = time.perf_counter()
for _ in range(1_000_000): os.write(fd, b'x') # 1,000,000 calls
print((time.perf_counter() - t) * 1000, 'ms')Three runs of the byte-at-a-time version took 643, 665 and 697 ms (an earlier set of six runs on the same VM ranged from 494 to 736 ms). The chunked version, 244 full 4 KB writes plus one short one, took 0.22 to 0.24 ms. Counting with strace -f -c: 1,000,004 write calls for the first, 249 for the second; the few extra are Python's own startup writes and exact counts shift slightly between runs and machines. That is the whole reason C's printf and similar buffered I/O libraries collect output in a buffer and flush it in blocks. The kernel crossing is the expensive part, not the copying of one byte.
Second, calls that really enter the kernel against one that does not. In C, with 2,000,000 iterations each, run twice:
syscall(SYS_getppid); /* always traps */
clock_gettime(CLOCK_REALTIME, &ts); /* libc, via vDSO */
syscall(SYS_clock_gettime, CLOCK_REALTIME, &ts); /* forced trap */Results in nanoseconds per call, first run then second: the forced getppid trap took 735 and 439, the vDSO clock_gettime took 48 and 22, and the same clock_gettime forced through the kernel with syscall() took 642 and 369. So the same clock call, sent through the kernel instead of the vDSO, was roughly ten times slower or more. The two runs differ a lot, so read these as rough ranges, not constants.
In Python, 1,000,000 calls each, best of three: time.time() cost about 74 ns per call and os.getppid() about 350 ns. strace counts the getppid calls (3,000,000 from three repeats) but shows no time or clock_gettime entries, because the vDSO call never traps. getppid is used instead of getpid because libc may cache the process ID.
07Common interview questions
Q: What is the difference between user mode and kernel mode?
A: They are CPU privilege states. In kernel mode the CPU allows privileged instructions and access to all memory and devices; in user mode it does not. Programs run in user mode and enter kernel mode only through controlled entry points: system calls, interrupts and exceptions.
Q: What happens when a program calls read()?
A: The libc wrapper puts the call number and arguments in registers and executes a trap instruction. The CPU switches to kernel mode and a kernel stack, the kernel indexes its system call table with the number and runs the handler, then returns the result to user mode. The wrapper converts a negative result into errno and -1.
Q: How is an interrupt different from a system call?
A: An interrupt is asynchronous and comes from hardware, such as a timer or a network card, at an unpredictable moment. A system call is synchronous and deliberate: the program executes the trap itself. Exceptions such as page faults are synchronous but caused by the instruction, not requested.
Q: Is a system call a context switch?
A: No. It enters the kernel inside the same process, with no change of address space. A context switch to another process can happen afterwards if the call blocks or the scheduler decides to preempt.
Q: Why is writing one byte at a time slow, and what fixes it?
A: The crossing dominates the cost. Each write is one trap, so a million one-byte writes means about a million crossings. In a measured run that took roughly two thirds of a second, against a fraction of a millisecond for the same data in 4 KB chunks. Buffering batches data so there are fewer crossings.
Q: Why does a C library wrapper exist at all?
A: It hides the register conventions and call numbers, and it translates the kernel's negative return value into errno and -1. Some calls, such as the ones in the vDSO, may not enter the kernel at all.