11 Multitasking and interrupts — a machine taking turns
What to know first
Looking back
chapter 10 said that processes each appear to run alone even though there are many of them. But on a machine with only one place to compute, that “simultaneously” cannot be true. Only one thing is really happening: a single core does a little of this job, stops, does a little of that one, stops, and repeats it very quickly.
Which raises three questions at once. Who decides the taking of turns? When is a job cut off — after so many instructions? when the program asks? or at fixed intervals of time? And how is that “time to cut off” announced to the core?
A. The answers to those three are the frame of this chapter.
Who — the scheduler inside the operating system. The policy for who gets how much lives there.
When — there are two occasions. One is when the program stops of its own accord (waiting for a disk or for input, it has nothing more to do). The other is when the allotted time runs out. Counting instructions is not used; why not is something this chapter shows.
How — a timer device stops the core at fixed intervals. That stopping is exactly an interrupt. So multitasking and interrupts are different things that meet at this one place, where policy has to drive hardware.
The need for this chapter, and its context
One honest word first. An interrupt is not something you use directly from a C program. Standard C has no syntax for raising or receiving one, and the rest of this book never handles an interrupt directly. Judged by usefulness alone, this chapter could be skipped.
Three reasons keep it here anyway. First, it is not unrelated — the standard signal (chapter 81) inherited this very structure. Second, there is no better key to what happens inside the CPU: the contention, and the switching between one piece of work and another. Third, this was once the basis of almost everything a personal computer did — that is the story of the DOS years.
And this chapter deliberately puts two things side by side. Multitasking is a policy — how time is to be shared. An interrupt is a device — how the outside announces an event to the core. Their purposes are plainly different, yet the policy only works by driving that device, so the two must be understood apart and then understood together. If chapter 10 said what a process is, this chapter says how those processes come to take turns.
So read this chapter not to use it but to understand the machine better. That much is enough.
By the end of this chapter
The questions this chapter answers
- Then why have interrupts at all, if you have to ask anyway?
- Can interruptions be blocked altogether, then?
11.1 One core, several jobs — the idea of taking turns#
On a machine with one place to compute, the only way to run two programs “at once” is to alternate between them in very small pieces. Go back and forth faster than the eye can follow and, to a person, the two appear to run together.
This is multitasking. But “taking turns” hides three things that have to be decided.
Three things have to be decided. Who decides — who picks the job to run next; the answer is the scheduler inside the operating system. When to cut off — what occasion stops the running job; the answer is two occasions, “when it stops by itself” and “when the time runs out”. And how to announce it — how that occasion reaches the core; the answer is an interrupt from the timer device.
The third row is where multitasking and interrupts meet in this chapter. The first two are policy; the last one is a device.
11.2 When to cut off — three candidates#
“When do we cut off?” could be answered in several ways. Listing the candidates and crossing them out shows why today’s answer looks as it does.
| Candidate | What would happen | Used? | Why |
|---|---|---|---|
| After a fixed number of instructions | the core counts instructions and stops at the limit | No | it can be counted; but instructions take wildly different times, so it would not be fair |
| When the program gives way | it says “I am waiting now” and control passes on | Yes | there is no reason to idle the core while it waits for I/O — but this alone is not enough |
| After a fixed interval of time | a timer stops the core at regular intervals | Yes | it can take the machine back from a program that never gives way — without it, one program could own the machine |
| When something more urgent wakes | a job that was waiting becomes ready and pushes the current one aside | Yes | work where response matters (input, sound) should not queue behind everything else |
Table 11.1 — Candidate answers to “when do we cut off?”
★ Why the first row is crossed out matters. It is often said that hardware does not count instructions for you — it does. Modern cores carry a performance monitoring unit that counts retired instructions and can raise an interrupt when the counter overflows.
The reasons lie elsewhere. First, instruction count is not proportional to elapsed time (one that waits for memory takes tens of times longer — chapter 12), so “a thousand instructions” means a different amount of time each occasion. Second, those counters are a scarce resource, not a general clock to reach for at will. Third, what the operating system is protecting is fairness and response, and those are measured in time, not in instructions. So we cut by time.
11.2.1 Why giving way is not enough by itself#
There were systems that used only the second row: control passes only when the running program says “I step back for a moment”. This is called cooperative, or non-preemptive.
The trouble is right there in the word “cooperative”. If one program does not cooperate, nothing else runs. A long calculation, an endless loop, or a forgotten yield, and the whole machine stops. The mouse stops moving.
Which is why the third row is needed — a device that takes the machine back from outside, regardless of a program’s good will. That way of doing it is called preemptive.
11.3 The kinds of multitasking#
Now the kinds can be laid out. History ran roughly in this order.
| Kind | Running at once | Who decides the switch | Strength | Its disease |
|---|---|---|---|---|
| Task switcher | one | the person, by switching screens | simple; works when memory is scarce (put one to sleep, wake the other) | what you switched away from stops — a download stops too |
| Cooperative | several | the running program, when it yields | switch points are known, so data stays safe | one program that never yields stops everything |
| Preemptive | several | the operating system, when time runs out | no single program can own the machine | it can be cut anywhere, so data must be protected |
| Time-sharing | several (and several people) | the operating system, fairly among people | many people share one machine | switching is frequent, and it costs |
Table 11.2 — Ways of taking turns
★ The first row, the task switcher, is not multitasking at all. Several programs may be loaded, but only one ever runs. On a switch the outgoing program is put to sleep whole (sometimes written out to disk) and the incoming one wakes. It was the compromise of an age when memory was precious. Meet a device today where “switching works but nothing progresses in the background” and this is what you are looking at.
In practice. One program that will not yield stops the machine
In the years when cooperative multitasking was widespread this was an everyday experience. A program would begin a long calculation and the screen would freeze. Clicking another window did nothing, the clock stopped, and the pointer turned into an hourglass while nothing happened at all. Only that program yielding could let anything else run.
Moving to preemptive scheduling made the symptom disappear. A program can still fall into an endless loop today, but it does not take the operating system down with it — the timer takes the machine back.
11.4 What is carried across a switch — the context#
Changing jobs means replacing the core’s present state wholesale. That state is called the context, and replacing it is a context switch.
| What | Why it must be carried | If it is not |
|---|---|---|
| General registers | values in mid-calculation live there | you return and compute with someone else’s values |
| Program counter | how far execution got | there is no knowing where to return |
| Stack pointer | the top of your own stack | you use someone else’s stack as your own |
| Status bits | things like the result of a comparison | the very next branch goes the wrong way |
| Root of the address space | which table translates addresses (chapter 10) | someone else’s memory becomes visible |
Table 11.3 — What has to be carried across a switch
One cost does not appear in that table. After a switch the caches and the translation tables still hold what the previous job was using, so the newcomer starts out slow for a while. That “cold cache” is often more expensive than saving and restoring the registers (chapter 12).
★ So the interval between switches is a trade. Cut often and response improves while the share spent switching grows; cut rarely and the reverse. Operating systems choose this value differently, and the same system gives different values to different kinds of work.
11.5 Who announces the moment — the timer interrupt#
One question is left: who tells the core that the time has run out?
The core does not watch a clock by itself. A timer device stops it at fixed intervals instead, and that stopping is the other half of this chapter: the interrupt.
| Step | What happens | Who does it |
|---|---|---|
| 1 | the interval passes and the timer raises an interrupt | hardware |
| 2 | the core finishes the current instruction and jumps to the operating system’s handler | hardware |
| 3 | the time is counted, and the question is asked whether this job’s turn is over | the operating system |
| 4 | if it is, the context is replaced and the next job takes over | the operating system (the scheduler) |
| 5 | if not, control returns to what was running | the operating system |
Table 11.4 — What the timer interrupt does
★ Rows 3~5 are policy; rows 1~2 are the device. The same timer interrupt can carry either a cooperative or a preemptive policy on top of it — and in the DOS years an application program could hijack that interrupt and lay its own policy on it, as we see later in this chapter.
That is the story of taking turns. From here we look at the last link itself, the interrupt — for not only the timer but the keyboard, the disk and the communication line all speak to the core the same way.
11.6 The wires to a device — one after another, or all at once#
The card reader, the line printer and the teletype of chapter 9 were all attached to the computer by wires. How those wires are run decides much of a device’s character.
| One after another (serial) | All at once (parallel) | |
|---|---|---|
| Wires | One | Eight, for one byte |
| Per beat | 1 bit | 8 bits |
| Agreement needed | Mark “it starts here” and “it ends here” | A separate wire says “the value is valid now” |
| The hard part | At speed, the two clocks must agree closely | All eight must arrive at the same instant |
Table 11.5 — Two ways to send one byte
The teletype was the first kind. Bits went down a single wire in a row, and the receiver saw the “start” mark, set its clock by it, and sampled the value at fixed intervals. The thick cable to a printer was the second kind — eight wires carrying one byte at once, with a separate signal saying “valid now”.
Against intuition, the faster things get, the more serial wins. With eight wires each arrives at a slightly different moment, and as the speed rises that spread grows larger than the time of a single bit. So today’s fast links mostly took the other road: make one serial wire very fast, then bundle several of them.
One more thing. On a small machine a single wire to the outside may serve as input or as output, and which one it is the program announces by writing a value into a fixed place. This — “write into a fixed place and the device acts” — is the basic way of driving the outside, and that place is named by an address but is not memory: reading it gives the state right now, writing it makes something happen. That distinction is what later gives the language the word volatile (chapter 14).
However the wires are run, two problems remain. One is that the two sides work at different speeds — hence the tub of chapter 9. The other is the subject of this chapter: who decides when the outside speaks.
11.7 When the outside speaks#
chapter 9 said there is a tub on the incoming side. Then who puts characters into it? How does a program learn that a key was pressed?
There are two ways.
| Asking (polling) | Interrupting | |
|---|---|---|
| Who moves | My program keeps checking | The device stops the CPU |
| When nothing happens | Asking still costs time | Costs nothing |
| When something happens | Late by up to one asking interval | Noticed at once |
| Shape of the code | Runs in order | The flow is cut and resumed |
Table 11.6 — Two ways to notice that something happened outside
Polling is the program asking “yet? yet?” over and over. It is simple, but anything that happens between two questions waits for the next one, and the asking goes on even when there is nothing to find.
Interruption is the reverse. The device announces that something happened and stops the CPU. The CPU halts right where it is, jumps to a piece of code registered in advance, handles the event, and returns to the place it left.
| Step | What happens | Does the program know? |
|---|---|---|
| 1 | The device announces that something happened | No |
| 2 | The CPU finishes the current instruction and stops | No |
| 3 | The place to return to is written down somewhere | No |
| 4 | Control jumps to the handler decided in advance | No |
| 5 | When the handler is done, control returns there | It continues as if nothing had happened |
Table 11.7 — The order of events in an interruption
This is usually how characters come to be sitting in the input buffer. While my program is busy with something else, the keyboard interrupts, drops one character into the tub and withdraws. That is why it is already there when I go to read.
One thing becomes plain here: an interruption can happen between any two lines of my code, and my code cannot tell that it did.
A common misconception. The program I wrote runs in the order I wrote it, without a break
11.7.1 When several things interrupt#
There is more than one device. A timer, a keyboard, a communication line each announce that something happened. So the interruptions collide with one another too.
| What | The situation | So the machine… |
|---|---|---|
| Overlap | Another arrives while the first is being handled | decides who goes first — a priority |
| Nesting | An urgent one cuts into the one being handled | allows it or forbids it — it differs by machine |
| Waiting | It cannot be taken now, so only “there was a request” is recorded | handles it when it can |
| Loss | The same device asks again before being served | usually keeps only that there was a request, not how many |
| Delay | Urgent work waits behind less urgent work | and that delay is this machine’s limit on response |
Table 11.8 — What happens when several things interrupt
★ The fourth row is the surprising one. Interruptions are not counted. “There was a request” is normally a single on/off flag, so if ten more arrive before it is served, the handler still runs once. That is why code dealing with the outside does not count how often it was called; it asks the device what happened.
11.7.2 What “asking the device” actually means#
We just said the code “asks the device”. But how does one ask? Send it a letter? Call some function?
Reading a fixed address is the asking. There is no new syntax and no special function.
Outside devices, we said, are driven by writing to and reading from fixed places. Think of such a place as the device’s counter — a window it keeps open. The window is named by an address but it is not memory. When the CPU puts that address on the wires, the device wired to it puts its current state back as a value. One read is the question, and the value that comes back is the answer — there is no separate exchange of messages.
The windows a device keeps open are usually these.
| Window | Reading it | Writing it | What is asked and answered |
|---|---|---|---|
| Status | gives the current situation as bits | usually not writable | “Is there something to take?” “Is there room to send?” “Did anything go wrong?” |
| Data | gives one value that arrived | puts in one value to send | “Give me that value” “Send this” |
| Control | reads the settings back | changes how the device behaves | “Use this speed” “Raise an interrupt for me” |
| Count | gives how many are waiting | usually not writable | “How many have arrived?” — some devices keep this window too |
Table 11.9 — The windows a device keeps open
Take an example. An interrupt has come from the device that receives characters — the one that fills the tub of chapter 9. But what if three bytes had arrived in the meantime?
Code that counts how often it was called takes one byte, because it was called once, and loses the other two. Code that asks does this instead.
void uart_isr(void) /* called when the interrupt fires */
{
while (*UART_STATUS & RX_READY) /* "anything to take?" --- the asking */
buffer_push(*UART_DATA); /* "here" --- one byte per read */
}*UART_STATUS reads the status window, and one bit of that value means “there is something to take”. If there is, the data window is read for one byte, and the question is asked again. Three bytes waiting means three turns of the loop; none waiting means no turns at all. How often the handler was called does not matter.
One property surprises people at first: reading changes the device’s state. Reading the data window removes that byte from the device’s queue, and if nothing is left the “something to take” bit in the status window goes down by itself. That is why the loop above stops on its own.
A common misconception. Reading only fetches a value; it changes nothing
True for memory. A device window is different. The moment you read, a value leaves the queue, a status bit clears, and sometimes the interrupt request itself is withdrawn. So the habit of “read it twice to be sure” becomes a bug in front of a device — the second read takes the next byte away.
Conversely, there are places where you must use this property. Some devices only drop their interrupt request once the status window has been read. If the handler skips that read, the request stays raised and the handler is called again the moment it returns, and again.
Q. Then why have interrupts at all, if you have to ask anyway?
A. The two do different jobs. An interrupt is a doorbell; asking is opening the door to see who is there. Without the bell you must keep opening the door (polling); with only the bell you never learn who came. So real code uses both — the interrupt says when to look, and asking says what arrived.
Q. Can interruptions be blocked altogether, then?
A. They can be held off for a while. Mark “not taking any now” and the requests that arrive will wait. But urgent work is delayed along with the rest, and if you hold them off for long, some are lost. So the rule is to keep the blocked stretch short, and to remember the previous state and put it back rather than simply switching interrupts on at the end.
11.7.3 When another arrives mid-handler — how existing code copes#
The “nesting” row above is the one that troubles real code most. If another interrupt arrives while a handler is running, the same code may start again before it has finished.
Machine and code cope together. The machine first: most chips disable interrupts automatically the moment they jump into a handler. On a machine with priorities the rule is “block the same level and below, let more urgent ones in”, so nesting does not happen at random — it happens only for something more urgent.
On top of that sits the discipline of the code. From the oldest device drivers to today’s, five things recur.
| Way | What it means | What it costs |
|---|---|---|
| Keep the handler short | The handler records the fact and leaves — “a key was pressed” is noted, and the real work is done later by the main flow | The work is deferred a little |
| Make it safe to re-enter | Inside the handler use only local variables and share as little as possible with the outside — this is called being re-entrant | Fewer functions may be called from a handler |
| Share only what moves in one step | Values shared by the main flow and the handler are kept small enough to read or write in one go; if they cannot be, block interrupts briefly while touching them | Other work is delayed while blocked |
| Block briefly, then restore | Block only around the part that touches the value, and remember the previous state and put it back exactly | Ending with “just switch them on” breaks someone else’s rule |
| Budget the nesting depth | Count how many levels can stack up and reserve that much stack in advance | Memory has to be set aside up front |
Table 11.10 — Five ways code copes when interrupts overlap
★ The first row covers most of the rest. A short handler both reduces overlap and gets out of it quickly. That is why old device code has handlers that are startlingly short — move one byte into the tub, set one flag saying there is work to do, and that is all.
The same shape repeats on top of an operating system. While a signal handler runs, the same signal is usually blocked, and what a handler may do is defined very narrowly. Why those rules exist is taken up, with the rules themselves, in chapter 81.
11.8 How the two differ — interrupts and multitasking#
Having come this far you can see the two meeting in one place — which is exactly why they should be held apart. They look alike, and their purposes are plainly different.
| Interrupt | Multitasking | |
|---|---|---|
| What it is | a device announcing an outside event to the core | a policy for sharing out time |
| Purpose | to notice without being late | to divide fairly, and to leave nothing idle |
| Occasion | comes from outside — a key, a timer, a disk | set by policy — time ran out, or the job stopped |
| Afterwards | control returns to what was running | control moves to another job |
| Without it | you must keep asking (polling) | one program owns the machine |
| Layer | a contract set by the hardware | a rule set by the operating system |
Table 11.11 — Interrupts and multitasking
★ The fourth row separates them most sharply. Returning to what was running is the default for an interrupt; switching is a choice the operating system lays on top. A timer interrupt does not necessarily mean moving to another job — if time remains, control simply returns.
Conversely multitasking can exist without interrupts. Cooperative scheduling does: switch only where a program yields and no timer is needed. What disease that suffers from we have already seen.
Splitting the same moment between the two: when the timer raises an interrupt the hardware finishes the current instruction, jumps to the handler, and holds interrupts off while it runs. Meanwhile the operating system counts the time and asks whether this job’s turn is over. And as the handler ends the hardware restores the context as it was — except that the operating system may have put a different context there.
The last cell of the third row is where a “switch” actually happens. On the way back from an interrupt the operating system swaps the place it will return to. As far as the hardware is concerned it is merely returning from a handler — but on arrival, a different program is running.
11.9 It was once almost everything a personal computer did#
Today interrupts have sunk below the kernel and are hard to see. In the first age of the personal computer they sat close to the user. This is the story of the DOS years.
Machines of that era had a table saying, for each interrupt number, “when this number arrives, jump here”, and that table lived at the very start of memory (see chapter 4 for why the neighbourhood of address 0 is special). The striking part is that an application program could write its own address into that table.
| What | How | Today |
|---|---|---|
| Calling the operating system | To open a file you “threw” a fixed number — not a function call but an interrupt | It survives as the system call (the mechanism is similar) |
| Keyboard input | A key press raised an interrupt whose handler put the character in the tub | Unchanged — but inside the kernel |
| The clock | An interrupt at fixed intervals counted the time | Unchanged — and task switching was built on top of it |
| Resident programs | They hijacked one slot of the table, inserted their own code and stepped back | Gone — the operating system forbids it |
| Running several programs | The clock interrupt sliced execution up and handed the pieces out in turn | A basic function of the operating system |
Table 11.12 — What interrupts did in the DOS years
DOS also had a task switcher: several programs loaded, screens swapped between them, but whatever went to the back was put to sleep whole — the first row of the table of kinds above. On a machine with about 640 KiB of memory even that was a great convenience.
★ The fourth and fifth rows say the most about that era. DOS ran one program at a time, yet by hijacking the clock interrupt an application could slice execution into pieces and run several programs in turn. Such products existed — and it was an application, not an operating system, that did it.
In practice. What it meant to hijack a slot of the table
A terminate-and-stay-resident program lived like this. On starting it remembered the value in the table slot it was after, wrote the address of its own code there, and withdrew, staying in memory. When that interrupt fired, its code ran first, and when it was done it passed control to the address it had remembered.
The method was as precarious as it was powerful. If two of them hijacked the same slot the order became a chain, and if even one forgot to pass control on, everything behind it died. That is why today’s operating systems do not hand this place to application programs.
11.10 Where this story survives today#
DOS is gone; the structure remains. Here is only where what you saw in this chapter leads.
| From this chapter | Where it leads | Which chapter |
|---|---|---|
| The flow is taken over | The operating system runs processes in turn | the ground under the switching of chapter 10 |
| A handler may do very little | The narrow rules of a signal handler | chapter 81 |
| Is a single line indivisible? | Atomic operations and memory order | chapter 85 |
| A value may change at any moment | The word volatile | chapter 14 |
| Code that drives the outside directly | Programming a machine with no operating system | chapter 104 |
Table 11.13 — Where the ideas of this chapter lead
Three sentences are worth taking from this chapter. Outside devices are driven by writing to and reading from fixed places. An interrupt can arrive at any time, and when several arrive they collide. And on top of that stands the appearance of several programs running at once.
You will probably never use one. But the sense of how a machine takes turns between jobs — and why some code must be written on the assumption that anything can cut in at any moment — stays with you.