Kernel

Contents

Kernel and processes

The kernel is the operating system software that runs with full machine privilege, meaning full privilege over all machine resources.

Processes are running software programs without full machine privilege. (Processes are often called unprivileged processes or user-level processes to emphasize their unprivileged status. “User level” is the opposite of “kernel”.)

A process is a program in execution. The relationship between a program and a process is like that between a recipe and a cake. A recipe is an inanimate list of instructions that, interpreted by a cook, can make something delicious. A program is an inanimate list of instructions—a file on disk—that, loaded into memory and interpreted by a processor, can make something magical. A process is a live instance of a program, running at a particular time, on a particular piece of hardware, dealing with a particular set of inputs.

The kernel’s purpose is to serve the needs of processes as a whole. It balances three goals:

  1. Fairly share machine resources among processes.
  2. Provide safe and convenient access to machine resources by inventing abstractions for those resources (such as files, which abstract disks).
  3. Ensure robustness and performance.

In modern operating systems, much kernel code aims to provide protection: ensuring that no process can violate the operating system’s sharing policies. This is because processes can have bugs. A process can crash, or enter an infinite loop, or attempt to take over the machine, maliciously or accidentally. So kernels should prevent mistakes in individual processes from bringing down the system as a whole.

Kernels can achieve these goals only with help from hardware. A running process executes on a processor; that processor executes the process’s instructions, one after another, as fast as possible. The kernel is not emulating the processor—that would be very slow. The processor does not validate each instruction with the kernel. Instead, the processor runs on behalf of the process, and executes most process instructions directly. Processors support special mechanisms, accessible only to privileged code (the kernel), that ensure that processes cannot run amok, and the kernel can still arbitrate resources.

Resource: Processor time

One of the most fundamental machine resources is processor time (or CPU time): the fraction of time the processor spends executing one process’s instructions rather than another’s. The kernel aims to share processor time according to its policy.

Here’s a fundamental attack on fair sharing of processor time. It’s the worst attack in the world:

int main() {
    while (true) {
    }
}

An infinite loop. Compiled to x86-64 instructions, this might be

00000000000005fa <main>:
 5fa:   55                      push   %rbp
 5fb:   48 89 e5                mov    %rsp,%rbp
 5fe:   eb fe                   jmp    5fe <main+0x4>

The critical instruction is jmp 5fe, represented in bytes as eb fe, which spins the processor in a tight loop forever.

Aside. Why is this loop represented as 0xeb 0xfe? An instruction consists of an opcode (e.g., “push”, “mov”, “pop”) and some operands (e.g., “%rbp”, “5fe”). Here, the 0xeb part is the opcode. This opcode means “unconditional branch (jmp) by a relative one-byte offset”: when the instruction is executed, the %rip register will be modified by adding to it the signed offset stored as an operand. Here, that operand is 0xfe, which, considered as a signed 8-bit number, is -2. Remember that when an instruction executes, the initial value of %rip is always the address of the next instruction (because the processor must read the entire current instruction before executing it). Thus, adding -2 to %rip will reset %rip back to the start of the jmp.

Processors generally execute the instructions they’re given in a simple-minded, straightforward way. If a processor starts executing an infinite loop, how will any other instruction ever run?

We need a way to limit the time that any single process can run on the CPU. After that time elapses, the processor should interrupt its execution and switch to the kernel, giving the kernel a chance to run something else.

Machines accomplish this with a separate piece of hardware called the timer. This timer can be configured by the kernel to go off periodically in real time, such as once every millisecond. When the timer goes off, it sends an interrupt to the processor, which gives the processor the chance to run something else.

Timer interrupts are an almost inevitable consequence of the problem of infinite loops. Many other aspects of timer interrupt implementation also follow logically from the problem timer interrupts aim to solve.