Your Program Cannot Read a File, and Never Could
The separation between your code and the kernel is enforced by the processor, not agreed by convention. Here is what your code genuinely cannot do, and the single door it uses to ask.
A bit in the processor
Open a file in any language and the line looks like every other line. A name
goes in, something comes back, and the vocabulary is the vocabulary of ordinary
code.
It is not ordinary code. Your program did not open anything. It asked, and
something else did it, and the reason it had to ask is not politeness or
library design. It is that the processor your program is running on is in a
state where the instructions required to open a file are refused.
Processors have at least two of these states. In the stronger one, every
instruction works. In the weaker one, a specific set of instructions fault
instead of executing: anything that talks to a device, anything that changes
how addresses are translated, anything that affects interrupts, anything that
reconfigures the machine itself.
Your program runs in the weaker state, always. The kernel runs in the stronger
one. That is the whole separation, and the important thing about it is where it
lives: in a register in the processor, which no instruction available to your
program can change.
| your code may | the kernel may | faults if your code trie | |
|---|---|---|---|
| arithmetic on your own d | 1 | 1 | 0 |
| calling your own functio | 1 | 1 | 0 |
| reading your own memory | 1 | 1 | 0 |
| allocating within memory | 1 | 1 | 0 |
| an infinite loop | 1 | 1 | 0 |
| writing to a disk contro | 0 | 1 | 1 |
| changing an address mapp | 0 | 1 | 1 |
The list of things you cannot do
It is worth having the list, because every system call you will ever use exists
to cover one of its entries.
Your code cannot reach a device. Disks, network cards, keyboards and timers are
reached through addresses and ports the processor will not let you touch. So
every file read, every packet sent and every character printed is a request.
Your code cannot change what its own addresses mean. The mapping from the
addresses in your program to actual memory is maintained by the kernel, in
structures your code cannot write. So growing the heap is a request, and so is
mapping a file, and so is sharing a page with another process.
Your code cannot see another process. Not its memory, not its registers. Any
communication at all, a pipe, a socket, a shared page, a signal, goes through
the kernel because there is no other path between two address spaces.
Your code cannot stop being interrupted. The instruction that masks interrupts
is privileged, which is exactly why no program can monopolise a processor by
refusing to be taken off it.
And your code cannot set the clock, mount a filesystem, change its own
privileges, or any of the other machine-wide operations, for the same reason in
each case.
Notice what is left. Computation on data you already hold. That is it. Every
program worth running therefore spends its life asking, and the rest of this
course is about what happens during the asking.
One door, and the kernel picks the room
The request mechanism is smaller than people imagine.
put the request number in one agreed register
put up to six arguments in other agreed registers
execute the one instruction that traps
the processor then:
switches to the stronger state
saves where your code was
jumps to an address the kernel set at boot
the kernel then:
looks up the number in its table
checks the arguments are permissible
does the work, or refuses
puts a result in the agreed register
returns, switching the state backTwo properties of this design do all the work.
Your code names a number, not a destination. The number is an index into a
table the kernel controls, so the set of things that can be asked for is fixed
by the kernel and cannot be extended by a program.
And the arguments are checked, every time, on the kernel's side of the
boundary. A pointer you pass is validated against what your process is actually
allowed to touch before anything is read through it. This is slow, it is the
reason a request costs what the next lesson will measure, and skipping it has
historically been the source of the most serious bugs in operating systems.
Why convention would not do
The obvious objection is that all of this could be done in software. A library
that checks before acting, a compiler that refuses to emit the dangerous
instructions, a rule that everybody follows.
None of those hold, for the same reason. A software check is code, and code is
in an address space, and a program determined to do otherwise can jump past it,
or overwrite it, or simply never call it. A compiler's refusal binds only
programs that went through that compiler. A convention binds only the
well-behaved, and the well-behaved were never the problem.
| step | step | what the program does | what the processor does | where control is | what happened |
|---|---|---|---|---|---|
| 1 | 1 | computes the device address | nothing special | in the program | Entirely legal. Arithmetic is arithmetic, and there is no rule against knowing an address. |
| 2 | 2 | issues the write | checks the current state | in the program | The check is made by the hardware on this instruction, not by anything the program called. There is nothing to jump past. |
| 3 | 3 | nothing, it never executed | raises a fault | in the kernel | Control has left the program at the faulting instruction. The instruction had no effect at all. |
| 4 | 4 | receives a signal, or dies | resumed in the weaker state | wherever the kernel decided | The program does not get to choose what happens next, which is the part that makes this a boundary rather than a warning. |
The cost of this is what the next lesson is about. A boundary that is enforced
by the hardware is a boundary that has to be crossed by a specific mechanism,
and that mechanism is thousands of times more expensive than a function call.
Knowing how much more expensive, and why, explains a surprising amount of
otherwise mysterious performance.
Recap
- The separation is a hardware state, so a program cannot bypass it by being clever, and the instructions that matter simply fault when your code runs them.
- Your code cannot touch a device, change what its own addresses mean, or see another process, which is why almost everything interesting is a request rather than an action.
- There is one door, your code chooses only the number and the arguments, and the kernel chooses where control lands, which is the property that makes the whole arrangement safe.
This is the reading half
Starting the course gives you your own copy of it. Every idea on every page has problems standing under it, marked with a reason rather than a tick, and any sentence you do not believe can be opened and argued with. None of that can happen on a page nobody owns.
The contents