Memra

What happens when you call read()

◈ 9 cards

Name the four entry points into the kernel and trace one system call from the wrapper, through the trap and the dispatch table, to the service routine and back.

The kernel is not a library

Every program you have written so far has called functions. strlen(), printf(), a function of your own — the call pushes some arguments, jumps to an address in your own address space, and comes back. The kernel is not reachable that way. It lives in memory your process is not allowed to read, running with privileges your process does not have, and a plain call instruction into it would be a security hole with a calling convention.

So the transition is made deliberately, at exactly four places. These are the four entry points into the kernel, and naming them is a standing exam question:

  1. Interrupt — raised by a peripheral device. The disk controller finished a transfer; the network card has a frame; the timer ticked. Asynchronous, and nothing to do with what the CPU was executing.
  2. Trap — raised by the CPU itself while executing an instruction. Divide by zero, a bad memory reference, an illegal opcode. Synchronous: it is caused by the instruction that is running.
  3. Signal — a software interrupt, delivered to a process by the kernel or by another process. SIGINT from your Ctrl-C, SIGSEGV after the trap above.
  4. System call — the running program asking the kernel for a service.

The split that matters: the first two are hardware-driven and the last two are software-driven. Interrupts and traps happen to a process; signals and system calls are things a program (or the kernel on a program's behalf) does.

Tracing one call: read(3, buf, 512)

What you write is an ordinary C function call, and that is the point — the awkward part is hidden inside a wrapper, a small library function that exists only to cross the boundary. Follow it:

(i) Your code calls the wrapper. read in libc is not the kernel's read. It is six or seven instructions of glue.

(ii) The wrapper sets up and traps. It places the three arguments where the kernel expects them, puts the system call number for read in a register, and executes the machine's trap instruction. That instruction is the boundary: it switches the CPU from user mode to kernel mode and transfers control to a single kernel routine, conventionally called syscall().

(iii) syscall() dispatches. It reads the call number, copies the arguments out of the user stack and into the kernel stack — user memory cannot be trusted to stay put — and uses the number as an index into the dispatch table, an array of pointers to the service routines. Entry N is the kernel's read.

(iv) The service routine runs. Now in kernel mode, with kernel privileges, it locates the open file, moves bytes into your buffer, advances the offset, and leaves two things behind: a return value in a register, and an error code in another.

(v) The wrapper unwinds. Control comes back to the library glue, which inspects the return value. On failure it stores the error code in the per-thread variable your program knows as errno and returns −1. On success it returns whatever the kernel returned.

(vi) Your program continues at the instruction after the trap, none the wiser.

The return convention, once, for all of them

Across the whole system-call interface: −1 means failure, and the reason is in errno. Not a message, not an exception, not a printed warning — a value you must look at. perror("open") prints your label, a colon, and the text for the current errno, which is why every one of this module's examples ends a failed call with it.

errno is only meaningful immediately after a call that reported failure. A successful call is allowed to leave any value in it, so testing errno without first testing the return value is a bug that works by accident most of the time.

Why a library call is never cheaper than the system call it wraps

This is the standard follow-up question, and the answer falls straight out of the trace. fputs() is a library call; underneath, it eventually calls write(). It cannot be faster than write() in the sense of doing less — it does everything write() does plus its own bookkeeping.

What it can do is call write() fewer times. fputs() copies your bytes into a buffer in your own address space and only traps when the buffer fills. Writing a thousand short lines through fputs() might cost three system calls; writing them through write() costs a thousand. Each of those thousand pays for a mode switch, an argument copy, and a dispatch. So the buffered interface is faster per byte while being strictly more expensive per call — and that is the whole trade between the C standard library's stream I/O (FILE *, buffered) and low-level I/O (int descriptors, unbuffered).

an ordinary C callset up registersprivilege switchcall number Nvalue in a registeruser coderead(3, buf, 512)library wrapperargs + call numbertrap instructionuser mode -> kernel modesyscall() dispatchindex the dispatch tableservice routinethe kernel does the workreturn value + errno-1 means it did not happenControl returns to thewrapper, which stores theerror code where errno willfind it, then to theinstruction after the trap.
The trap instruction is the boundary. Everything left of it runs in user mode; everything right of it runs in kernel mode.
Entry pointRaised byDriven byExampleinterrupta peripheral devicehardwarethe disk finished atransfertrapthe CPU, on anexceptionhardwaredivide by zerosignalthe kernel oranother processsoftwareSIGINT from yourCtrl-Csystem callthe running programitselfsoftwareread() asking forbytesAn interrupt is asynchronous; a trap is caused by the instruction executing at that moment.
The four entry points into the kernel. Two are hardware-driven, two are software-driven — that split is the examinable half.
NORMAL ~/memra/learn/comp-325/system-calls-and-the-kernel-boundary utf-8 LF