Basis Lab

Answers · How a program runs

Why does my program have to ask the operating system just to read a file?

In shortThe processor refuses to run the instructions that touch a disk or change memory mappings in your program's limited mode, so your code must trap into the kernel and ask it to do the read.

Because the instructions that touch a device or change how addresses are translated are refused by the processor while your program is running. Opening or reading a file is therefore a request to the kernel, not a function call into a library.

A file read needs a device, or at least memory the kernel manages on behalf of a device. Processors have at least two states. In the stronger one every instruction works. In the weaker one a set of instructions faults instead of executing: anything that talks to a device, anything that changes address translation, anything that affects interrupts, anything that reconfigures the machine. Your program runs in the weaker state. The kernel runs in the stronger state. The state is held in a processor register that your program has no instruction to change, so this is not a rule your code agrees to follow. It is a condition your code cannot alter.

FIG 1A store to a device, step by step
stepstepyour programthe processorcontrol iswhat happened
1firstworks out the address of the devicenothing specialin your programArithmetic on a number is legal, even if the number happens to be a device address.
2secondissues the store to that addresschecks the current state at the instructionin your programThe check is made by the processor, not by anything the program called.
3thirdnothing: the store never happensraises a faultin the kernelThe store has no effect. Control leaves your program at the faulting instruction.
4fourthreceives a signal or is killedreturns to the weaker state when the kernel allows itwhere the kernel decidesYour program does not choose the next instruction after the fault.
4 steps
The refusal is made by hardware as part of executing the instruction. There is no library call to skip and no branch to invert. Control leaves your program at the faulting instruction and lands where the kernel decided.

The important step is the second one. Your program can compute the address of a device, because arithmetic is arithmetic. It can put that address in a register. What it cannot do is execute the store. The processor checks the current state as part of executing that store and raises a fault before the store has any effect. There is no call to avoid, no branch to invert, no byte to patch, because the enforcement is not made of instructions your program can reach. Control leaves your program at the faulting instruction and lands at an address the kernel fixed before your program ran.

What is left is computation on data your program already holds. It can add, compare, call its own functions, read its own memory, and loop forever. It cannot reach a device, change what its own addresses mean, see another process, or stop being interrupted. Every system call exists to cover one of those gaps. A file read covers the first gap: the data is on a device, or in a kernel buffer the kernel controls, and your code cannot fetch it directly.

handler=table[number]\mathrm{handler} = \mathrm{table}[\mathrm{number}]

The request mechanism is small. A library function puts a request number in one agreed register and a fixed set of arguments in other agreed registers. Then it executes the one instruction that traps. The processor switches to the stronger state, saves where your code was, and jumps to a single address the kernel set at boot. The kernel looks up the number in its table, checks the arguments against what your process is allowed to touch, does the work or refuses, puts a result in an agreed register, and returns. Your program supplied a number and some arguments. It did not supply a destination, because if it could, the destination would be the whole security problem.

FIG 2A request, at the level the processor sees
plaintext
put the request number in one agreed register
put the arguments in the other agreed registers
execute the one instruction that traps

the processor switches to the stronger state,
saves where your code was, and jumps
to the address the kernel set at boot

the kernel looks up the number,
checks the arguments, does the work or refuses,
puts a result in the agreed register, and returns
The request is a number and arguments, then one trap. The kernel registered the entry point before any program ran, so the program never chooses where control lands.

The number is an index into a table the kernel controls. That is the second property that matters. The set of things a program can ask for is fixed by the kernel, not extended by the program. The arguments are checked on the kernel side of the boundary, every time. A pointer passed to a read is validated against the calling process before anything is read through it. That check costs time, and it is the reason a request is not free. Skipping it has been the source of serious operating system bugs, because the caller is by assumption not trustworthy.

Why a library cannot enforce it

The natural guess is that the operating system is a library your code calls, so the overhead is a convention or a design choice. The guess fails because a software check is code. Code lives in an address space. A program determined to do otherwise can jump past the check, overwrite it, or never call it at all. A compiler that refuses to emit the dangerous instruction binds only programs compiled by that compiler. A convention binds only the well behaved, and the well behaved were never the problem. The check has to be made by something the program cannot rewrite. That something is the processor, using a state bit and a fault.

The same hardware check explains the cost. A request saves state, switches state, enters the kernel, validates arguments, does the work, and switches back. The switch is far more expensive than a function call because it is a deliberate crossing of a boundary, not a jump within your own code. The validation adds more. The work itself can add anything from almost nothing to a long wait for a disk, which is why a profiler that only sees your functions shows a blank where most of the time went.

Where the boundary is absent

This stops being true if your code already runs in the stronger state. A kernel module or a driver can touch the device and the request disappears. In an ordinary process, a library that looks local is only a wrapper around the trap. It can make the request easier to write. It cannot make the request unnecessary.

Worth keeping

  • A file read is a request because the processor, not a library, forbids your code from touching devices or address mappings directly.
  • Your code chooses a request number and arguments; the kernel chooses the handler and checks the pointers.
  • A library call can wrap the request but cannot remove the boundary crossing.

Where this goes next

This question is one step of What Happens While You Wait, a course that works through its subject from the beginning. The page that teaches this part of it is Two Worlds and a Door, and it reads the whole way through whether or not you have an account.

What an account changes is the practice: problems standing under every idea, marked with a reason rather than a tick, in a copy of the course that is yours.

Read that pageThe whole course

Other questions people ask about this

  1. Why is a system call so much slower than calling a function?A system call is slow because the processor has to switch into the kernel and back, and that fixed cost is paid in full even when the request itself is trivial.
  2. Why does 2 + 3 * 4 equal 14 and not 20?The multiplication is done first: three times four makes twelve, and only then is the two added, giving fourteen. Reading left to right would give twenty.
  3. How many samples does a profiler need before a percentage in the report means anything?It depends on how many samples landed on that line: you need a few hundred samples on the line you care about, because the relative error is roughly one over the square root of that count.
  4. If I make one part of my program twice as fast, how much faster does the whole program get?Less than twice as fast: only the share of run time you actually touch can improve, so the untouched remainder caps the gain, often at a few per cent.
  5. Why does reading one byte from memory load 64 bytes?Memory hardware moves data in fixed sixty-four-byte blocks, so a one-byte read fetches the whole block; the extra bytes cost almost nothing because reaching the memory row costs most of the time.
  6. Why can two threads that each work correctly still give the wrong answer?Each thread reads the shared value and writes it back in two separate steps, so the other thread can run in between and one increment silently overwrites the other.

Read alongside